diff --git a/.dialyzer-ignore b/.dialyzer-ignore index 2002338a16b7..6fdcd343342b 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -31,6 +31,11 @@ lib/block_scout_web/templates/address_contract/index.html.eex:170 lib/block_scout_web/templates/address_contract/index.html.eex:207 lib/explorer/third_party_integrations/sourcify.ex:159 lib/explorer/third_party_integrations/sourcify.ex:162 +lib/explorer/smart_contract/solidity/verifier.ex:317 +lib/block_scout_web/templates/address_contract/index.html.eex:158 +lib/block_scout_web/templates/address_contract/index.html.eex:195 +lib/explorer/third_party_integrations/sourcify.ex:120 +lib/explorer/third_party_integrations/sourcify.ex:123 lib/block_scout_web/views/transaction_view.ex:137 lib/block_scout_web/views/transaction_view.ex:152 lib/block_scout_web/views/transaction_view.ex:197 diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index a2df4b3551c0..087c4ba91dba 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -639,6 +639,7 @@ jobs: ADMIN_PANEL_ENABLED: "true" ACCOUNT_ENABLED: "true" ACCOUNT_REDIS_URL: "redis://localhost:6379" + API_V2_ENABLED: "true" - name: Upload Unit Test Results if: always() uses: actions/upload-artifact@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index cabe874c457f..e8cf3d3d74d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,14 @@ ### Features +- [#5561](https://github.com/blockscout/blockscout/pull/5561), [#6523](https://github.com/blockscout/blockscout/pull/6523) - Improve working with contracts implementations +- [#6401](https://github.com/blockscout/blockscout/pull/6401) - Add Sol2Uml contract visualization +- [#6481](https://github.com/blockscout/blockscout/pull/6481) - Smart contract verification improvements +- [#6444](https://github.com/blockscout/blockscout/pull/6444) - Add support for yul verification via rust microservice +- [#6440](https://github.com/blockscout/blockscout/pull/6440) - Add support for base64 encoded NFT metadata - [#6407](https://github.com/blockscout/blockscout/pull/6407) - Indexed ratio for int txs fetching stage - [#6324](https://github.com/blockscout/blockscout/pull/6324) - Add verified contracts list page -- [#6379](https://github.com/blockscout/blockscout/pull/6379) - API v2 for frontend +- [#6379](https://github.com/blockscout/blockscout/pull/6379), [#6429](https://github.com/blockscout/blockscout/pull/6429) - API v2 for frontend - [#6351](https://github.com/blockscout/blockscout/pull/6351) - Enable forum link env var - [#6316](https://github.com/blockscout/blockscout/pull/6316) - Copy public tags functionality to master - [#6196](https://github.com/blockscout/blockscout/pull/6196) - INDEXER_CATCHUP_BLOCKS_BATCH_SIZE and INDEXER_CATCHUP_BLOCKS_CONCURRENCY env varaibles @@ -18,11 +23,16 @@ - [#6391](https://github.com/blockscout/blockscout/pull/6391), [#6427](https://github.com/blockscout/blockscout/pull/6427) - TokenTransfer token_id -> token_ids migration - [#6443](https://github.com/blockscout/blockscout/pull/6443) - Drop internal transactions order index - [#6450](https://github.com/blockscout/blockscout/pull/6450) - INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE and INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY env variables +- [#6454](https://github.com/blockscout/blockscout/pull/6454) - INDEXER_RECEIPTS_BATCH_SIZE, INDEXER_RECEIPTS_CONCURRENCY, INDEXER_COIN_BALANCES_BATCH_SIZE, INDEXER_COIN_BALANCES_CONCURRENCY env variables +- [#6476](https://github.com/blockscout/blockscout/pull/6476), [#6484](https://github.com/blockscout/blockscout/pull/6484) - Update token balances indexes ### Fixes +- [#6532](https://github.com/blockscout/blockscout/pull/6532) - Fix index creation migration +- [#6473](https://github.com/blockscout/blockscout/pull/6473) - Fix state changes for contract creation transactions +- [#6475](https://github.com/blockscout/blockscout/pull/6475) - Fix token name with unicode graphemes shortening - [#6420](https://github.com/blockscout/blockscout/pull/6420) - Fix address logs search -- [#6390](https://github.com/blockscout/blockscout/pull/6390) - Fix transactions responses in API v2 +- [#6390](https://github.com/blockscout/blockscout/pull/6390), [#6502](https://github.com/blockscout/blockscout/pull/6502), [#6511](https://github.com/blockscout/blockscout/pull/6511) - Fix transactions responses in API v2 - [#6357](https://github.com/blockscout/blockscout/pull/6357), [#6409](https://github.com/blockscout/blockscout/pull/6409), [#6428](https://github.com/blockscout/blockscout/pull/6428) - Fix definitions of NETWORK_PATH, API_PATH, SOCKET_ROOT: process trailing slash - [#6338](https://github.com/blockscout/blockscout/pull/6338) - Fix token search with space - [#6329](https://github.com/blockscout/blockscout/pull/6329) - Prevent logger from truncating response from rust verifier service in case of an error @@ -42,6 +52,7 @@ - [#6336](https://github.com/blockscout/blockscout/pull/6336) - Fix sending request on each key in token search - [#6327](https://github.com/blockscout/blockscout/pull/6327) - Fix and refactor address logs page and search - [#6449](https://github.com/blockscout/blockscout/pull/6449) - Search min_missing_block_number from zero +- [#6492](https://github.com/blockscout/blockscout/pull/6492) - Remove token instance owner fetching ### Chore diff --git a/apps/block_scout_web/assets/js/pages/sol2uml.js b/apps/block_scout_web/assets/js/pages/sol2uml.js new file mode 100644 index 000000000000..253f268a6bf8 --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/sol2uml.js @@ -0,0 +1,82 @@ +import 'viewerjs/dist/viewer.min.css' +import Viewer from 'viewerjs' +import $ from 'jquery' +import { createStore, connectElements } from '../lib/redux_helpers.js' + +export const initialState = { + contract_svg: null, + visualize_error: null +} + +export function reducer (state = initialState, action) { + switch (action.type) { + case 'SVG_FETCHED': { + return Object.assign({}, state, { + contract_svg: action.contract_svg, + visualize_error: action.error + }) + } + default: + return state + } +} + +const elements = { + '[data-selector="contract-image"]': { + render ($el, state, oldState) { + if (state.contract_svg) { + $('#spinner').hide() + $('#gallery img').attr('src', 'data:image/svg+xml;base64,' + state.contract_svg) + const gallery = document.getElementById('gallery') + const viewer = new Viewer(gallery, { + inline: false, + toolbar: { + zoomIn: 2, + zoomOut: 4, + oneToOne: 4, + reset: 4, + play: { + show: 4, + size: 'large' + }, + rotateLeft: 4, + rotateRight: 4, + flipHorizontal: 4, + flipVertical: 4 + } + }) + viewer.update() + $el.show() + } else if (state.visualize_error) { + $('#spinner').hide() + $el.empty().text('Cannot visualize contract: ' + state.visualize_error) + $el.show() + } else { + $('#spinner').show() + $el.hide() + } + } + } +} + +function loadSvg (store) { + const $element = $('[data-async-contract-svg]') + const path = $element.data().asyncContractSvg + + function fetchSvg () { + $.getJSON(path) + .done((response) => { + store.dispatch(Object.assign({ type: 'SVG_FETCHED' }, response)) + }) + } + + fetchSvg() +} + +function main () { + const store = createStore(reducer) + connectElements({ store, elements }) + loadSvg(store) +} + +main() diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 8dc80804fd2b..1830a34d4240 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -15,14 +15,14 @@ "assert": "^2.0.0", "bignumber.js": "^9.1.0", "bootstrap": "^4.6.0", - "chart.js": "^3.9.1", - "chartjs-adapter-luxon": "^1.2.0", + "chart.js": "^4.0.1", + "chartjs-adapter-luxon": "^1.3.0", "clipboard": "^2.0.11", "core-js": "^3.26.1", "crypto-browserify": "^3.12.0", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", - "highlight.js": "^11.6.0", + "highlight.js": "^11.7.0", "https-browserify": "^1.0.0", "humps": "^2.0.1", "jquery": "^3.6.1", @@ -45,7 +45,7 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.1.0", + "luxon": "^3.1.1", "moment": "^2.29.4", "nanomorph": "^5.4.3", "numeral": "^2.0.6", @@ -53,30 +53,32 @@ "path-parser": "^6.1.0", "phoenix": "file:../../../deps/phoenix", "phoenix_html": "file:../../../deps/phoenix_html", + "photoswipe": "^5.3.4", "pikaday": "^1.8.2", "popper.js": "^1.14.7", "reduce-reducers": "^1.0.4", "redux": "^4.2.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "sweetalert2": "^11.6.8", + "sweetalert2": "^11.6.15", "uniqid": "^5.4.0", "urijs": "^1.19.11", "url": "^0.11.0", "util": "^0.12.5", + "viewerjs": "^1.11.1", "web3": "^1.8.1", - "web3modal": "^1.9.9", + "web3modal": "^1.9.10", "xss": "^1.0.14" }, "devDependencies": { - "@babel/core": "^7.20.2", + "@babel/core": "^7.20.5", "@babel/preset-env": "^7.20.2", "autoprefixer": "^10.4.13", "babel-loader": "^9.1.0", "copy-webpack-plugin": "^11.0.0", "css-loader": "^5.2.7", "css-minimizer-webpack-plugin": "^4.2.2", - "eslint": "^8.27.0", + "eslint": "^8.28.0", "eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.6.0", @@ -86,14 +88,14 @@ "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", "jest-junit": "^10.0.0", - "mini-css-extract-plugin": "^2.6.1", + "mini-css-extract-plugin": "^2.7.1", "postcss": "^8.4.19", - "postcss-loader": "^7.0.1", + "postcss-loader": "^7.0.2", "sass": "^1.56.1", "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "webpack": "^5.75.0", - "webpack-cli": "^4.10.0" + "webpack-cli": "^5.0.0" }, "engines": { "node": "16.x", @@ -3981,34 +3983,42 @@ } }, "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.0.tgz", + "integrity": "sha512-war4OU8NGjBqU3DP3bx6ciODXIh7dSXcpQq+P4K2Tqyd8L5OjZ7COx9QXx/QdCIwL2qoX09Wr4Cwf7uS4qdEng==", "dev": true, + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.0.tgz", + "integrity": "sha512-NNxDgbo4VOkNhOlTgY0Elhz3vKpOJq4/PKeKg7r8cmYM+GQA9vDofLYyup8jS6EpUvhNmR30cHTCEIyvXpskwA==", "dev": true, - "dependencies": { - "envinfo": "^7.7.3" + "engines": { + "node": ">=14.15.0" }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.0.tgz", + "integrity": "sha512-Rumq5mHvGXamnOh3O8yLk1sjx8dB30qF1OeR6VC00DIR6SLJ4bwwUGKC4pE7qBFoQyyh0H9sAg3fikYgAqVR0w==", "dev": true, + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { @@ -5443,8 +5453,12 @@ } }, "node_modules/chart.js": { - "version": "3.9.1", - "license": "MIT" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.0.1.tgz", + "integrity": "sha512-5/8/9eBivwBZK81mKvmIwTb2Pmw4D/5h1RK9fBWZLLZ8mCJ+kfYNmV9rMrGoa5Hgy2/wVDBMLSUDudul2/9ihA==", + "engines": { + "pnpm": "^7.0.0" + } }, "node_modules/chartjs-adapter-luxon": { "version": "1.3.0", @@ -8951,6 +8965,42 @@ "node": ">= 0.10" } }, + "node_modules/globule/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globule/node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/good-listener": { "version": "1.2.2", "license": "MIT", @@ -9139,9 +9189,9 @@ } }, "node_modules/highlight.js": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.6.0.tgz", - "integrity": "sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==", + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz", + "integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==", "engines": { "node": ">=12.0.0" } @@ -9455,11 +9505,13 @@ } }, "node_modules/interpret": { - "version": "2.2.0", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/ip": { @@ -12160,9 +12212,9 @@ } }, "node_modules/loader-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", - "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -12822,9 +12874,9 @@ } }, "node_modules/mini-css-extract-plugin": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", - "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.1.tgz", + "integrity": "sha512-viOoaUFy+Z2w43VsGPbtfwFrr0tKwDctK9dUofG5MBViYhD1noGFUzzDIVw0KPwCGUP+c7zqLxm+acuQs7zLzw==", "dev": true, "dependencies": { "schema-utils": "^4.0.0" @@ -12849,7 +12901,9 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "3.0.4", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", "dev": true, "license": "ISC", "dependencies": { @@ -13901,6 +13955,14 @@ "resolved": "../../../deps/phoenix_html", "link": true }, + "node_modules/photoswipe": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.4.tgz", + "integrity": "sha512-SN+RWHqxJvdwzXJsh8KrG+ajjPpdTX5HpKglEd0k9o6o5fW+QHPkW8//Bo11MB+NQwTa/hFw8BDv2EdxiDXjNw==", + "engines": { + "node": ">= 0.12.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -14962,14 +15024,16 @@ } }, "node_modules/rechoir": { - "version": "0.7.0", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "license": "MIT", "dependencies": { - "resolve": "^1.9.0" + "resolve": "^1.20.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/redent": { @@ -17097,6 +17161,20 @@ "extsprintf": "^1.2.0" } }, + "node_modules/viewerjs": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/viewerjs/-/viewerjs-1.11.1.tgz", + "integrity": "sha512-/VQ2zalHLZJOGIwlxOBtxagLZwNvU3Bf+nm692XlhNFxjBXRxpCVn+GeqmRFg9jK1Y2+Wf8PPGxZgTDN4pHXww==" + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -17548,8 +17626,9 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, "node_modules/web3modal": { - "version": "1.9.9", - "license": "MIT", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/web3modal/-/web3modal-1.9.10.tgz", + "integrity": "sha512-gRByp+toRiADwkJLLGRXsnIVbLS1aJB71sJyryS6C7cF6jJ3cRN1LbPYEMObMyJkyjOZonx0CNZVAYGiD099aA==", "dependencies": { "detect-browser": "^5.1.0", "prop-types": "^15.7.2", @@ -17616,44 +17695,42 @@ } }, "node_modules/webpack-cli": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", - "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.0.tgz", + "integrity": "sha512-AACDTo20yG+xn6HPW5xjbn2Be4KUzQPebWXsDMHwPPyKh9OnTOJgZN2Nc+g/FZKV3ObRTYsGvibAvc+5jAUrVA==", "dev": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", + "@webpack-cli/configtest": "^2.0.0", + "@webpack-cli/info": "^2.0.0", + "@webpack-cli/serve": "^2.0.0", "colorette": "^2.0.14", - "commander": "^7.0.0", + "commander": "^9.4.1", "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=14.15.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x" + "webpack": "5.x.x" }, "peerDependenciesMeta": { "@webpack-cli/generators": { "optional": true }, - "@webpack-cli/migrate": { - "optional": true - }, "webpack-bundle-analyzer": { "optional": true }, @@ -17669,11 +17746,12 @@ "dev": true }, "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 10" + "node": "^12.20.0 || >=14" } }, "node_modules/webpack-merge": { @@ -20923,25 +21001,23 @@ } }, "@webpack-cli/configtest": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz", - "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.0.0.tgz", + "integrity": "sha512-war4OU8NGjBqU3DP3bx6ciODXIh7dSXcpQq+P4K2Tqyd8L5OjZ7COx9QXx/QdCIwL2qoX09Wr4Cwf7uS4qdEng==", "dev": true, "requires": {} }, "@webpack-cli/info": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz", - "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.0.tgz", + "integrity": "sha512-NNxDgbo4VOkNhOlTgY0Elhz3vKpOJq4/PKeKg7r8cmYM+GQA9vDofLYyup8jS6EpUvhNmR30cHTCEIyvXpskwA==", "dev": true, - "requires": { - "envinfo": "^7.7.3" - } + "requires": {} }, "@webpack-cli/serve": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz", - "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.0.tgz", + "integrity": "sha512-Rumq5mHvGXamnOh3O8yLk1sjx8dB30qF1OeR6VC00DIR6SLJ4bwwUGKC4pE7qBFoQyyh0H9sAg3fikYgAqVR0w==", "dev": true, "requires": {} }, @@ -21956,7 +22032,9 @@ "version": "0.0.2" }, "chart.js": { - "version": "3.9.1" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.0.1.tgz", + "integrity": "sha512-5/8/9eBivwBZK81mKvmIwTb2Pmw4D/5h1RK9fBWZLLZ8mCJ+kfYNmV9rMrGoa5Hgy2/wVDBMLSUDudul2/9ihA==" }, "chartjs-adapter-luxon": { "version": "1.3.0", @@ -24510,6 +24588,37 @@ "glob": "~7.1.1", "lodash": "~4.17.10", "minimatch": "~3.0.2" + }, + "dependencies": { + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + } } }, "good-listener": { @@ -24629,9 +24738,9 @@ } }, "highlight.js": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.6.0.tgz", - "integrity": "sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==" + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.7.0.tgz", + "integrity": "sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==" }, "hmac-drbg": { "version": "1.0.1", @@ -24854,7 +24963,9 @@ } }, "interpret": { - "version": "2.2.0", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true }, "ip": { @@ -26793,9 +26904,9 @@ "dev": true }, "loader-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz", - "integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -27304,9 +27415,9 @@ "peer": true }, "mini-css-extract-plugin": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", - "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.1.tgz", + "integrity": "sha512-viOoaUFy+Z2w43VsGPbtfwFrr0tKwDctK9dUofG5MBViYhD1noGFUzzDIVw0KPwCGUP+c7zqLxm+acuQs7zLzw==", "dev": true, "requires": { "schema-utils": "^4.0.0" @@ -27319,7 +27430,9 @@ "version": "1.0.1" }, "minimatch": { - "version": "3.0.4", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -28085,6 +28198,11 @@ "phoenix_html": { "version": "file:../../../deps/phoenix_html" }, + "photoswipe": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.4.tgz", + "integrity": "sha512-SN+RWHqxJvdwzXJsh8KrG+ajjPpdTX5HpKglEd0k9o6o5fW+QHPkW8//Bo11MB+NQwTa/hFw8BDv2EdxiDXjNw==" + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -28781,10 +28899,12 @@ } }, "rechoir": { - "version": "0.7.0", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "requires": { - "resolve": "^1.9.0" + "resolve": "^1.20.0" } }, "redent": { @@ -30322,6 +30442,20 @@ "extsprintf": "^1.2.0" } }, + "viewerjs": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/viewerjs/-/viewerjs-1.11.1.tgz", + "integrity": "sha512-/VQ2zalHLZJOGIwlxOBtxagLZwNvU3Bf+nm692XlhNFxjBXRxpCVn+GeqmRFg9jK1Y2+Wf8PPGxZgTDN4pHXww==" + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, "w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -30728,7 +30862,9 @@ } }, "web3modal": { - "version": "1.9.9", + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/web3modal/-/web3modal-1.9.10.tgz", + "integrity": "sha512-gRByp+toRiADwkJLLGRXsnIVbLS1aJB71sJyryS6C7cF6jJ3cRN1LbPYEMObMyJkyjOZonx0CNZVAYGiD099aA==", "requires": { "detect-browser": "^5.1.0", "prop-types": "^15.7.2", @@ -30796,22 +30932,23 @@ } }, "webpack-cli": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", - "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.0.0.tgz", + "integrity": "sha512-AACDTo20yG+xn6HPW5xjbn2Be4KUzQPebWXsDMHwPPyKh9OnTOJgZN2Nc+g/FZKV3ObRTYsGvibAvc+5jAUrVA==", "dev": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", + "@webpack-cli/configtest": "^2.0.0", + "@webpack-cli/info": "^2.0.0", + "@webpack-cli/serve": "^2.0.0", "colorette": "^2.0.14", - "commander": "^7.0.0", + "commander": "^9.4.1", "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "dependencies": { @@ -30822,7 +30959,9 @@ "dev": true }, "commander": { - "version": "7.2.0", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz", + "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==", "dev": true } } diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 9ed973b7b67b..83db599525e0 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -27,14 +27,14 @@ "assert": "^2.0.0", "bignumber.js": "^9.1.0", "bootstrap": "^4.6.0", - "chart.js": "^3.9.1", - "chartjs-adapter-luxon": "^1.2.0", + "chart.js": "^4.0.1", + "chartjs-adapter-luxon": "^1.3.0", "clipboard": "^2.0.11", "core-js": "^3.26.1", "crypto-browserify": "^3.12.0", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", - "highlight.js": "^11.6.0", + "highlight.js": "^11.7.0", "https-browserify": "^1.0.0", "humps": "^2.0.1", "jquery": "^3.6.1", @@ -57,7 +57,7 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.1.0", + "luxon": "^3.1.1", "moment": "^2.29.4", "nanomorph": "^5.4.3", "numeral": "^2.0.6", @@ -65,30 +65,32 @@ "path-parser": "^6.1.0", "phoenix": "file:../../../deps/phoenix", "phoenix_html": "file:../../../deps/phoenix_html", + "photoswipe": "^5.3.4", "pikaday": "^1.8.2", "popper.js": "^1.14.7", "reduce-reducers": "^1.0.4", "redux": "^4.2.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "sweetalert2": "^11.6.8", + "sweetalert2": "^11.6.15", "uniqid": "^5.4.0", "urijs": "^1.19.11", "url": "^0.11.0", "util": "^0.12.5", "web3": "^1.8.1", - "web3modal": "^1.9.9", + "web3modal": "^1.9.10", + "viewerjs": "^1.11.1", "xss": "^1.0.14" }, "devDependencies": { - "@babel/core": "^7.20.2", + "@babel/core": "^7.20.5", "@babel/preset-env": "^7.20.2", "autoprefixer": "^10.4.13", "babel-loader": "^9.1.0", "copy-webpack-plugin": "^11.0.0", "css-loader": "^5.2.7", "css-minimizer-webpack-plugin": "^4.2.2", - "eslint": "^8.27.0", + "eslint": "^8.28.0", "eslint-config-standard": "^17.0.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-n": "^15.6.0", @@ -98,14 +100,14 @@ "jest": "^29.3.1", "jest-junit": "^10.0.0", "jest-environment-jsdom": "^29.3.1", - "mini-css-extract-plugin": "^2.6.1", + "mini-css-extract-plugin": "^2.7.1", "postcss": "^8.4.19", - "postcss-loader": "^7.0.1", + "postcss-loader": "^7.0.2", "sass": "^1.56.1", "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "webpack": "^5.75.0", - "webpack-cli": "^4.10.0" + "webpack-cli": "^5.0.0" }, "jest": { "moduleNameMapper": { diff --git a/apps/block_scout_web/assets/webpack.config.js b/apps/block_scout_web/assets/webpack.config.js index ce9abe3e8699..d83e74de1a7e 100644 --- a/apps/block_scout_web/assets/webpack.config.js +++ b/apps/block_scout_web/assets/webpack.config.js @@ -59,6 +59,7 @@ const appJs = 'admin-tasks': './js/pages/admin/tasks.js', 'token-contract': './js/pages/token_contract.js', 'smart-contract-helpers': './js/lib/smart_contract/index.js', + 'sol2uml': './js/pages/sol2uml.js', 'token-transfers-toggle': './js/lib/token_transfers_toggle.js', 'epoch-tile': './js/lib/epoch_tile.js', 'campaign-banner': './js/lib/campaign_banner.js', @@ -87,6 +88,10 @@ const appJs = }, module: { rules: [ + { + test: /\.css$/, + use: ["style-loader", "css-loader"], + }, { test: /\.js$/, exclude: /node_modules/, 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 ce32c87e8fed..4887bf835d16 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 @@ -120,6 +120,7 @@ defmodule BlockScoutWeb.ApiRouter do scope "/addresses" do get("/:address_hash", V2.AddressController, :address) + get("/:address_hash/counters", V2.AddressController, :counters) get("/:address_hash/token-balances", V2.AddressController, :token_balances) get("/:address_hash/transactions", V2.AddressController, :transactions) get("/:address_hash/token-transfers", V2.AddressController, :token_transfers) @@ -130,9 +131,17 @@ defmodule BlockScoutWeb.ApiRouter do get("/:address_hash/coin-balance-history-by-day", V2.AddressController, :coin_balance_history_by_day) end + scope "/tokens" do + get("/:address_hash", V2.TokenController, :token) + get("/:address_hash/counters", V2.TokenController, :counters) + get("/:address_hash/transfers", V2.TokenController, :transfers) + get("/:address_hash/holders", V2.TokenController, :holders) + end + scope "/main-page" do get("/blocks", V2.MainPageController, :blocks) get("/transactions", V2.MainPageController, :transactions) + get("/indexing-status", V2.MainPageController, :indexing_status) end scope "/stats" do 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 7e3df9baf64a..fb7146839e82 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 @@ -151,7 +151,7 @@ defmodule BlockScoutWeb.AddressController do def address_counters(conn, %{"id" => address_hash_string}) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.hash_to_address(address_hash) do - {validation_count} = address_counters(address) + {validation_count} = Chain.address_counters(address) transactions_from_db = address.transactions_count || 0 token_transfers_from_db = address.token_transfers_count || 0 @@ -173,57 +173,4 @@ defmodule BlockScoutWeb.AddressController do }) end end - - defp address_counters(address) do - validation_count_task = - Task.async(fn -> - validation_count(address) - end) - - Task.start_link(fn -> - transaction_count(address) - end) - - Task.start_link(fn -> - token_transfers_count(address) - end) - - Task.start_link(fn -> - gas_usage_count(address) - end) - - [ - validation_count_task - ] - |> Task.yield_many(:infinity) - |> Enum.map(fn {_task, res} -> - case res do - {:ok, result} -> - result - - {:exit, reason} -> - raise "Query fetching address counters terminated: #{inspect(reason)}" - - nil -> - raise "Query fetching address counters timed out." - end - end) - |> List.to_tuple() - end - - def transaction_count(address) do - AddressTransactionsCounter.fetch(address) - end - - def token_transfers_count(address) do - AddressTokenTransfersCounter.fetch(address) - end - - def gas_usage_count(address) do - AddressTransactionsGasUsageCounter.fetch(address) - end - - defp validation_count(address) do - Chain.address_to_validation_count(address.hash) - end 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 717210dc5065..0e677fa567c6 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 @@ -9,6 +9,9 @@ defmodule BlockScoutWeb.API.V2.AddressController do current_filter: 1 ] + import BlockScoutWeb.PagingHelper, + only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1] + alias BlockScoutWeb.API.V2.{AddressView, BlockView, TransactionView} alias Explorer.{Chain, Market} alias Indexer.Fetcher.TokenBalanceOnDemand @@ -25,19 +28,11 @@ defmodule BlockScoutWeb.API.V2.AddressController do } ] - @transaction_with_tt_necessity_by_association [ + @token_transfer_necessity_by_association [ 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, - [token_transfers: :token] => :optional, - [token_transfers: :to_address] => :optional, - [token_transfers: :from_address] => :optional, - [token_transfers: :token_contract_address] => :optional, - :block => :required + :to_address => :optional, + :from_address => :optional, + :block => :optional } ] @@ -52,6 +47,24 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + def counters(conn, %{"address_hash" => address_hash_string}) do + with {: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)} do + {validation_count} = Chain.address_counters(address) + + transactions_from_db = address.transactions_count || 0 + token_transfers_from_db = address.token_transfers_count || 0 + address_gas_usage_from_db = address.gas_used || 0 + + json(conn, %{ + transactions_count: to_string(transactions_from_db), + token_transfers_count: to_string(token_transfers_from_db), + gas_usage_count: to_string(address_gas_usage_from_db), + validations_count: to_string(validation_count) + }) + end + end + def token_balances(conn, %{"address_hash" => address_hash_string}) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do token_balances = @@ -82,7 +95,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do results_plus_one = Chain.address_to_transactions_with_rewards(address_hash, options) {transactions, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page_params(next_page, transactions, params) + next_page_params = + next_page |> next_page_params(transactions, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -94,24 +108,26 @@ defmodule BlockScoutWeb.API.V2.AddressController do def token_transfers(conn, %{"address_hash" => address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do options = - @transaction_with_tt_necessity_by_association + @token_transfer_necessity_by_association |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) + |> Keyword.merge(token_transfers_types_options(params)) results_plus_one = - Chain.address_hash_to_token_transfers( + Chain.address_hash_to_token_transfers_new( address_hash, options ) - {transactions, next_page} = split_list_by_page(results_plus_one) + {token_transfers, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page_params(next_page, transactions, params) + next_page_params = + next_page |> next_page_params(token_transfers, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) |> put_view(TransactionView) - |> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) + |> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) end end @@ -134,7 +150,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do results_plus_one = Chain.address_to_internal_transactions(address_hash, full_options) {internal_transactions, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page_params(next_page, internal_transactions, params) + next_page_params = + next_page |> next_page_params(internal_transactions, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -156,7 +173,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {logs, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page_params(next_page, logs, params) + next_page_params = next_page |> next_page_params(logs, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -170,7 +187,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do results_plus_one = Chain.address_to_logs(address_hash, paging_options(params)) {logs, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page_params(next_page, logs, params) + next_page_params = next_page |> next_page_params(logs, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -197,7 +214,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do results_plus_one = Chain.get_blocks_validated_by_address(full_options, address_hash) {blocks, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page_params(next_page, blocks, params) + next_page_params = next_page |> next_page_params(blocks, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -207,14 +224,17 @@ defmodule BlockScoutWeb.API.V2.AddressController do end def coin_balance_history(conn, %{"address_hash" => address_hash_string} = params) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do + with {: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), :empty_items_with_next_page_params} do full_options = paging_options(params) results_plus_one = Chain.address_to_coin_balances(address_hash, full_options) {coin_balances, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page_params(next_page, coin_balances, params) + next_page_params = + next_page |> next_page_params(coin_balances, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) 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 24a3daf808e4..10533c1f0b59 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 @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, put_key_value_to_paging_options: 3, split_list_by_page: 1] - import BlockScoutWeb.PagingHelper, only: [select_block_type: 1] + import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1, select_block_type: 1] alias BlockScoutWeb.API.V2.TransactionView alias BlockScoutWeb.BlockTransactionController @@ -51,7 +51,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do {blocks, next_page} = split_list_by_page(blocks_plus_one) - next_page_params = next_page_params(next_page, blocks, params) + next_page_params = next_page |> next_page_params(blocks, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -70,7 +70,10 @@ defmodule BlockScoutWeb.API.V2.BlockController do {transactions, next_page} = split_list_by_page(transactions_plus_one) - next_page_params = next_page_params(next_page, transactions, params) + next_page_params = + next_page + |> next_page_params(transactions, params) + |> delete_parameters_from_next_page_params() conn |> put_status(200) 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 20f900066bd7..b61ef387772c 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 @@ -10,6 +10,11 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> render(:message, %{message: "Invalid parameter(s)"}) end + def call(conn, {:not_found, _, :empty_items_with_next_page_params}) do + conn + |> json(%{"items" => [], "next_page_params" => nil}) + end + def call(conn, {:not_found, _}) do conn |> put_status(:not_found) 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 417eaf4009a8..1851cd9a3ea9 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 @@ -29,7 +29,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do [from_address: :smart_contract] => :optional, [to_address: :smart_contract] => :optional }, - paging_options: %PagingOptions{page_size: 5} + paging_options: %PagingOptions{page_size: 6} ) conn @@ -37,4 +37,16 @@ defmodule BlockScoutWeb.API.V2.MainPageController do |> put_view(TransactionView) |> render(:transactions, %{transactions: recent_transactions}) end + + def indexing_status(conn, _params) do + indexed_ratio_blocks = Chain.indexed_ratio_blocks() + finished_indexing_blocks = Chain.finished_blocks_indexing?(indexed_ratio_blocks) + + json(conn, %{ + finished_indexing_blocks: finished_indexing_blocks, + finished_indexing: Chain.finished_indexing?(indexed_ratio_blocks), + indexed_blocks_ratio: indexed_ratio_blocks, + indexed_inernal_transactions_ratio: if(finished_indexing_blocks, do: Chain.indexed_ratio_internal_transactions()) + }) + 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 fa1a46a0c4e0..7624d3574cd1 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 @@ -50,11 +50,23 @@ defmodule BlockScoutWeb.API.V2.StatsController do "gas_used_today" => Enum.at(transaction_stats, 0).gas_used, "gas_prices" => gas_prices, "static_gas_price" => gas_price, - "market_cap" => Helper.market_cap(market_cap_type, exchange_rate) + "market_cap" => Helper.market_cap(market_cap_type, exchange_rate), + "network_utilization_percentage" => network_utilization_percentage() } ) end + defp network_utilization_percentage do + {gas_used, gas_limit} = + Enum.reduce(Chain.list_blocks(), {Decimal.new(0), Decimal.new(0)}, fn block, {gas_used, gas_limit} -> + {Decimal.add(gas_used, block.gas_used), Decimal.add(gas_limit, block.gas_limit)} + end) + + if Decimal.compare(gas_limit, 0) == :eq, + do: 0, + else: gas_used |> Decimal.div(gas_limit) |> Decimal.mult(100) |> Decimal.to_float() + end + def transactions_chart(conn, _params) do [{:history_size, history_size}] = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}]) 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 new file mode 100644 index 000000000000..79998a4aaa82 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -0,0 +1,63 @@ +defmodule BlockScoutWeb.API.V2.TokenController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.API.V2.TransactionView + alias Explorer.Chain + + import BlockScoutWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3] + import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + def token(conn, %{"address_hash" => address_hash_string}) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash)} do + conn + |> put_status(200) + |> render(:token, %{token: token}) + end + end + + def counters(conn, %{"address_hash" => address_hash_string}) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:not_found, {:ok, _}} <- {:not_found, Chain.token_from_address_hash(address_hash)} do + {transfer_count, token_holder_count} = Chain.fetch_token_counters(address_hash, 30_000) + + json(conn, %{transfers_count: to_string(transfer_count), token_holders_count: to_string(token_holder_count)}) + end + end + + def transfers(conn, %{"address_hash" => address_hash_string} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)} do + results_plus_one = Chain.fetch_token_transfers_from_token_hash(address_hash, paging_options(params)) + + {token_transfers, next_page} = split_list_by_page(results_plus_one) + + next_page_params = + next_page |> next_page_params(token_transfers, params) |> delete_parameters_from_next_page_params() + + conn + |> put_status(200) + |> put_view(TransactionView) + |> render(:token_transfers, %{token_transfers: token_transfers, next_page_params: next_page_params}) + end + end + + def holders(conn, %{"address_hash" => address_hash_string} = params) do + from_api = true + + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:not_found, {:ok, token}, _} <- + {:not_found, Chain.token_from_address_hash(address_hash), :empty_items_with_next_page_params} do + results_plus_one = Chain.fetch_token_holders_from_token_hash(address_hash, from_api, paging_options(params)) + {token_balances, next_page} = split_list_by_page(results_plus_one) + + next_page_params = + next_page |> next_page_params(token_balances, params) |> delete_parameters_from_next_page_params() + + conn + |> put_status(200) + |> render(:token_balances, %{token_balances: token_balances, next_page_params: next_page_params, token: token}) + end + 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 cce9bd771847..4b6a6c95992c 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 @@ -4,7 +4,14 @@ defmodule BlockScoutWeb.API.V2.TransactionController do import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1] import BlockScoutWeb.PagingHelper, - only: [paging_options: 2, filter_options: 1, method_filter_options: 1, type_filter_options: 1] + only: [ + delete_parameters_from_next_page_params: 1, + paging_options: 2, + filter_options: 2, + method_filter_options: 1, + token_transfers_types_options: 1, + type_filter_options: 1 + ] alias Explorer.Chain alias Explorer.Chain.Import @@ -22,6 +29,15 @@ defmodule BlockScoutWeb.API.V2.TransactionController do } @token_transfers_neccessity_by_association %{ + [from_address: :smart_contract] => :optional, + [to_address: :smart_contract] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + from_address: :required, + to_address: :required + } + + @token_transfers_in_tx_neccessity_by_association %{ [from_address: :smart_contract] => :optional, [to_address: :smart_contract] => :optional, [from_address: :names] => :optional, @@ -51,7 +67,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do transaction_hash, necessity_by_association: @transaction_necessity_by_association )}, - preloaded <- Chain.preload_token_transfers(transaction, @token_transfers_neccessity_by_association, false) do + preloaded <- + Chain.preload_token_transfers(transaction, @token_transfers_in_tx_neccessity_by_association, false) do conn |> put_status(200) |> render(:transaction, %{transaction: preloaded}) @@ -59,24 +76,21 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end def transactions(conn, params) do - filter_options = filter_options(params) - method_filter_options = method_filter_options(params) - type_filter_options = type_filter_options(params) + filter_options = filter_options(params, :validated) full_options = - Keyword.merge( - [ - necessity_by_association: @transaction_necessity_by_association - ], - paging_options(params, filter_options) - ) + [ + necessity_by_association: @transaction_necessity_by_association + ] + |> Keyword.merge(paging_options(params, filter_options)) + |> Keyword.merge(method_filter_options(params)) + |> Keyword.merge(type_filter_options(params)) - transactions_plus_one = - Chain.recent_transactions(full_options, filter_options, method_filter_options, type_filter_options) + transactions_plus_one = Chain.recent_transactions(full_options, filter_options) {transactions, next_page} = split_list_by_page(transactions_plus_one) - next_page_params = next_page_params(next_page, transactions, params) + next_page_params = next_page |> next_page_params(transactions, params) |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -146,18 +160,18 @@ defmodule BlockScoutWeb.API.V2.TransactionController do def token_transfers(conn, %{"transaction_hash" => transaction_hash_string} = params) do with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)} do full_options = - Keyword.merge( - [ - necessity_by_association: @token_transfers_neccessity_by_association - ], - paging_options(params) - ) + [necessity_by_association: @token_transfers_neccessity_by_association] + |> Keyword.merge(paging_options(params)) + |> Keyword.merge(token_transfers_types_options(params)) token_transfers_plus_one = Chain.transaction_to_token_transfers(transaction_hash, full_options) {token_transfers, next_page} = split_list_by_page(token_transfers_plus_one) - next_page_params = next_page_params(next_page, token_transfers, params) + next_page_params = + next_page + |> next_page_params(token_transfers, params) + |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -177,7 +191,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do {internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) - next_page_params = next_page_params(next_page, internal_transactions, params) + next_page_params = + next_page + |> next_page_params(internal_transactions, params) + |> delete_parameters_from_next_page_params() conn |> put_status(200) @@ -207,7 +224,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do {logs, next_page} = split_list_by_page(logs_plus_one) - next_page_params = next_page_params(next_page, logs, params) + next_page_params = + next_page + |> next_page_params(logs, params) + |> delete_parameters_from_next_page_params() conn |> put_status(200) 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 79828d108b71..a49e2ec0562f 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 @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.SmartContractController do alias BlockScoutWeb.AddressView alias Explorer.Chain + alias Explorer.Chain.SmartContract alias Explorer.SmartContract.{Reader, Writer} import Explorer.SmartContract.Solidity.Verifier, only: [parse_boolean: 1] @@ -24,8 +25,8 @@ defmodule BlockScoutWeb.SmartContractController do {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do implementation_address_hash_string = if contract_type == "proxy" do - address.hash - |> Chain.get_implementation_address_hash(address.smart_contract.abi) + address.smart_contract + |> SmartContract.get_implementation_address_hash() |> Tuple.to_list() |> List.first() || @burn_address else diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex index 6a1ea07b1730..73739141e7dd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex @@ -5,7 +5,6 @@ defmodule BlockScoutWeb.Tokens.TokenController do alias BlockScoutWeb.AccessHelpers alias Explorer.Chain - alias Explorer.Counters.{TokenHoldersCounter, TokenTransfersCounter} def show(conn, %{"id" => address_hash_string}) do redirect(conn, to: AccessHelpers.get_path(conn, :token_transfer_path, :index, address_hash_string)) @@ -14,7 +13,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do def token_counters(conn, %{"id" => address_hash_string}) do case Chain.string_to_address_hash(address_hash_string) do {:ok, address_hash} -> - {transfer_count, token_holder_count} = fetch_token_counters(address_hash, 30_000) + {transfer_count, token_holder_count} = Chain.fetch_token_counters(address_hash, 30_000) json(conn, %{transfer_count: transfer_count, token_holder_count: token_holder_count}) @@ -22,34 +21,4 @@ defmodule BlockScoutWeb.Tokens.TokenController do not_found(conn) end end - - defp fetch_token_counters(address_hash, timeout) do - total_token_transfers_task = - Task.async(fn -> - TokenTransfersCounter.fetch(address_hash) - end) - - total_token_holders_task = - Task.async(fn -> - TokenHoldersCounter.fetch(address_hash) - end) - - [total_token_transfers_task, total_token_holders_task] - |> Task.yield_many(timeout) - |> Enum.map(fn {_task, res} -> - case res do - {:ok, result} -> - result - - {:exit, reason} -> - Logger.warn("Query fetching token counters terminated: #{inspect(reason)}") - 0 - - nil -> - Logger.warn("Query fetching token counters timed out.") - 0 - end - end) - |> List.to_tuple() - end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex index 14b07de0409d..c8e4e19feef8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex @@ -43,8 +43,7 @@ defmodule BlockScoutWeb.TransactionStateController do [from_address: :names] => :optional, [to_address: :names] => :optional, from_address: :required, - to_address: :required, - token: :required + to_address: :required }, # we need to consider all token transfers in block to show whole state change of transaction paging_options: %PagingOptions{key: nil, page_size: nil} @@ -66,11 +65,11 @@ defmodule BlockScoutWeb.TransactionStateController do to_hash = transaction.to_address_hash miner_hash = block.miner_hash - from = transaction.from_address - from_after = do_update_coin_balance_from_tx(from_hash, transaction, from_before, block) - from_coin_entry = if from_hash not in [to_hash, miner_hash] do + from = transaction.from_address + from_after = do_update_coin_balance_from_tx(from_hash, transaction, from_before, block) + View.render_to_string( TransactionStateView, "_state_change.html", @@ -84,11 +83,11 @@ defmodule BlockScoutWeb.TransactionStateController do ) end - to = transaction.to_address - to_after = do_update_coin_balance_from_tx(to_hash, transaction, to_before, block) - to_coin_entry = - if to_hash != miner_hash do + if not is_nil(to_hash) and to_hash != miner_hash do + to = transaction.to_address + to_after = do_update_coin_balance_from_tx(to_hash, transaction, to_before, block) + View.render_to_string( TransactionStateView, "_state_change.html", @@ -146,7 +145,9 @@ defmodule BlockScoutWeb.TransactionStateController do ) end - json(conn, %{items: Enum.sort([from_coin_entry, to_coin_entry, miner_entry | items])}) + json(conn, %{ + items: [from_coin_entry, to_coin_entry, miner_entry | items] |> Enum.reject(&is_nil/1) |> Enum.sort() + }) else {:restricted_access, _} -> TransactionController.set_not_found_view(conn, transaction_hash_string) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex index 457b52765939..9ce2bb54315a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex @@ -33,8 +33,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do [from_address: :names] => :optional, [to_address: :names] => :optional, from_address: :required, - to_address: :required, - token: :required + to_address: :required } ], paging_options(params) 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 new file mode 100644 index 000000000000..3863fb31a8c6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex @@ -0,0 +1,67 @@ +defmodule BlockScoutWeb.VisualizeSol2umlController do + use BlockScoutWeb, :controller + alias Explorer.Chain + alias Explorer.Visualize.Sol2uml + + def index(conn, %{"type" => "JSON", "address" => address_hash_string}) do + address_options = [ + necessity_by_association: %{ + :smart_contract => :optional + } + ] + + if Sol2uml.enabled?() do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true), + # 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 + |> Enum.map(fn additional_source -> {additional_source.file_name, additional_source.contract_source_code} end) + |> Enum.into(%{}) + |> Map.merge(%{ + get_contract_filename(address.smart_contract.file_path) => address.smart_contract.contract_source_code + }) + + params = %{ + sources: sources + } + + case Sol2uml.visualize_contracts(params) do + {:ok, svg} -> json(conn, %{"address" => address.hash, "contract_svg" => svg, "error" => nil}) + {:error, error} -> json(conn, %{"address" => address.hash, "contract_svg" => nil, "error" => error}) + end + else + _ -> json(conn, %{error: "contract not found or unverified"}) + end + else + not_found(conn) + end + end + + def index(conn, %{"address" => address_hash_string}) do + address_options = [ + necessity_by_association: %{ + :smart_contract => :optional + } + ] + + with true <- Sol2uml.enabled?(), + {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do + render(conn, "index.html", + address: address, + get_svg_path: visualize_sol2uml_path(conn, :index, %{"type" => "JSON", "address" => address_hash_string}) + ) + else + _ -> not_found(conn) + end + end + + def index(conn, _) do + not_found(conn) + end + + def get_contract_filename(nil), do: "main.sol" + def get_contract_filename(filename), do: filename +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 3f44c6d62acf..9b0b37224018 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 @@ -9,6 +9,7 @@ defmodule BlockScoutWeb.PagingHelper do @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"] def paging_options(%{"block_number" => block_number_string, "index" => index_string}, [:validated | _]) do with {block_number, ""} <- Integer.parse(block_number_string), @@ -32,23 +33,34 @@ defmodule BlockScoutWeb.PagingHelper do def paging_options(_params, _filter), do: [paging_options: @default_paging_options] - def filter_options(%{"filter" => filter}) do - parse_filter(filter, @allowed_filter_labels) + def token_transfers_types_options(%{"type" => filters}) do + [ + token_type: filters |> String.upcase() |> parse_filter(@allowed_token_transfer_type_labels) + ] + end + + def token_transfers_types_options(_), do: [token_type: []] + + # sobelow_skip ["DOS.StringToAtom"] + def filter_options(%{"filter" => filter}, fallback) do + filter = filter |> parse_filter(@allowed_filter_labels) |> Enum.map(&String.to_atom/1) + if(filter == [], do: [fallback], else: filter) end - def filter_options(_params), do: [] + def filter_options(_params, fallback), do: [fallback] + # sobelow_skip ["DOS.StringToAtom"] def type_filter_options(%{"type" => type}) do - parse_filter(type, @allowed_type_labels) + [type: type |> parse_filter(@allowed_type_labels) |> Enum.map(&String.to_atom/1)] end - def type_filter_options(_params), do: [] + def type_filter_options(_params), do: [type: []] def method_filter_options(%{"method" => method}) do - parse_method_filter(method) + [method: parse_method_filter(method)] end - def method_filter_options(_params), do: [] + def method_filter_options(_params), do: [method: []] def parse_filter("[" <> filter, allowed_labels) do filter @@ -56,13 +68,11 @@ defmodule BlockScoutWeb.PagingHelper do |> parse_filter(allowed_labels) end - # sobelow_skip ["DOS.StringToAtom"] def parse_filter(filter, allowed_labels) when is_binary(filter) do filter |> String.split(",") |> Enum.filter(fn label -> Enum.member?(allowed_labels, label) end) |> Enum.uniq() - |> Enum.map(&String.to_atom/1) end def parse_method_filter("[" <> filter) do @@ -114,4 +124,16 @@ defmodule BlockScoutWeb.PagingHelper do }, block_type: "Block" ] + + def delete_parameters_from_next_page_params(params) when is_map(params) do + params + |> Map.delete("block_hash_or_number") + |> Map.delete("transaction_hash") + |> Map.delete("address_hash") + |> Map.delete("type") + |> Map.delete("method") + |> Map.delete("filter") + end + + def delete_parameters_from_next_page_params(_), do: nil end 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 043da8bdaf34..1d4ba4858d24 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 @@ -141,24 +141,23 @@ <% end %> <%= if @is_proxy do %> - <% {implementation_address, name} = Chain.get_implementation_address_hash(@address.hash, @address.smart_contract.abi) || "0x0000000000000000000000000000000000000000" %> - <%= if implementation_address do %> -
-
- <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", - text: gettext("Implementation address of the proxy contract.") %> - <%= gettext("Implementation") %> -
-
- <%= link( - (if name, do: name <> " | " <> implementation_address, else: implementation_address), - to: address_path(@conn, :show, implementation_address), - class: "contract-address" - ) - %> -
-
- <% end %> + <% {implementation_address_, name} = SmartContract.get_implementation_address_hash(@address.smart_contract) %> + <% implementation_address = implementation_address_ || "0x0000000000000000000000000000000000000000" %> +
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip_2.html", + text: gettext("Implementation address of the proxy contract.") %> + <%= gettext("Implementation") %> +
+
+ <%= link( + (if name, do: name <> " | " <> implementation_address, else: implementation_address), + to: address_path(@conn, :show, implementation_address), + class: "contract-address" + ) + %> +
+
<% end %>
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 f44929c8453d..22caf6a77111 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 @@ -5,6 +5,7 @@ <% 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 %> +<% visualize_sol2uml_enabled = Explorer.Visualize.Sol2uml.enabled?() %>
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> @@ -32,7 +33,7 @@
<%= render BlockScoutWeb.CommonComponentsView, "_info.html" %> <%= gettext("Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB") %> <%= link( - metadata_for_verification.address_hash, + metadata_for_verification.address_hash, to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)) %>.
<%= gettext("All metadata displayed below is from that contract. In order to verify current contract, click") %> <%= gettext("Verify & Publish") %> <%= gettext("button") %>
<%= link(gettext("Verify & Publish"), to: path, class: "button button-primary button-sm float-right ml-3", "data-test": "verify_and_publish") %> @@ -119,9 +120,22 @@

<%= target_contract.file_path || gettext "Contract source code" %>

- +
+ <%= if visualize_sol2uml_enabled && !target_contract.is_vyper_contract && !is_nil(target_contract.abi) do %> + + +
+ + Sol2uml +
new
+
+
+
+ <% end %> + +
><%= target_contract.contract_source_code %>
         
@@ -138,18 +152,35 @@
<% end)%> -
-
-

<%= gettext "Contract ABI" %>

- -
-
-
<%= format_smart_contract_abi(target_contract.abi) %>
-            
-
-
+ <%= if !is_nil(target_contract.compiler_settings) do %> +
+
+

<%= gettext "Compiler Settings" %>

+ +
+
+
<%= format_smart_contract_abi(target_contract.compiler_settings) %>
+              
+
+
+ <% end %> + + <%= if !is_nil(target_contract.abi) do %> +
+
+

<%= gettext "Contract ABI" %>

+ +
+
+
<%= format_smart_contract_abi(target_contract.abi) %>
+              
+
+
+ <% end %> <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex index fd98aaf149e9..bd2a179d80ee 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex @@ -1,6 +1,6 @@
- <%= label @f, :compiler_version, gettext("Compiler") %> + <%= label :smart_contract, :compiler_version, gettext("Compiler") %>
<%= select @f, :compiler_version, @compiler_versions, class: "form-control border-rounded", "aria-describedby": "compiler-help-block", id: "smart_contract_compiler_version" %> <%= error_tag @f, :compiler_version, id: "compiler-help-block", class: "text-danger form-error" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex index fcc2109fc373..c269a84e79a1 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex @@ -1,17 +1,17 @@
- <%= label @f, "Try to fetch constructor arguments automatically" %> + <%= label @f, :autodetect_constructor_args, gettext("Try to fetch constructor arguments automatically") %>
<%= radio_button @f, :autodetect_constructor_args, false, class: "form-check-input autodetectfalse" %>
- <%= label :autodetect_constructor_args, :false, gettext("No"), class: "radio-text" %> + <%= label @f, :autodetect_constructor_args_false, gettext("No"), class: "radio-text" %>
<%= radio_button @f, :autodetect_constructor_args, true, class: "form-check-input autodetecttrue", "aria-describedby": "autodetect_constructor_args-help-block" %>
- <%= label :autodetect_constructor_args, :true, gettext("Yes"), class: "radio-text" %> + <%= label @f, :autodetect_constructor_args_true, gettext("Yes"), class: "radio-text" %>
<%= error_tag @f, :autodetect_constructor_args, id: "autodetect_constructor_args-help-block", class: "text-danger form-error" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex index 275de35cbec9..c452549a7069 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex @@ -1,21 +1,21 @@
-
- <%= label @f, "Include nightly builds" %> -
-
-
- <%= radio_button @f, :nightly_builds, false, checked: true, class: "form-check-input nightly-builds-false" %> -
- <%= label :nightly_builds, :false, gettext("No"), class: "radio-text" %> -
-
- <%= radio_button @f, :nightly_builds, true, class: "form-check-input nightly-builds-true", "aria-describedby": "nightly_builds-help-block" %> -
- <%= label :nightly_builds, :true, gettext("Yes"), class: "radio-text" %> -
-
- <%= error_tag @f, :nightly_builds, id: "nightly_builds-help-block", class: "text-danger form-error" %> +
+ <%= label @f, :nightly_builds, gettext("Include nightly builds") %> +
+
+
+ <%= radio_button @f, :nightly_builds, false, checked: true, class: "form-check-input nightly-builds-false" %> +
+ <%= label @f, :nightly_builds_false, gettext("No"), class: "radio-text" %> +
+
+ <%= radio_button @f, :nightly_builds, true, class: "form-check-input nightly-builds-true", "aria-describedby": "nightly_builds-help-block" %> +
+ <%= label @f, :nightly_builds_true, gettext("Yes"), class: "radio-text" %>
-
Select yes if you want to show nightly builds.
+
+ <%= error_tag @f, :nightly_builds, id: "nightly_builds-help-block", class: "text-danger form-error" %>
+
<%= gettext("Select yes if you want to show nightly builds.") %>
+
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_libraries_other.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_libraries_other.html.eex index 7c6af84faf51..104730d49a27 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_libraries_other.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_libraries_other.html.eex @@ -1,8 +1,7 @@ <%= for library_index <- 2..Application.get_env(:block_scout_web, :verification_max_libraries) do %> - <% library = "library" <> to_string(library_index) |> String.to_atom() %> -
- <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_name.html", library: library, index: library_index %> +
+ <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_name.html", index: library_index %> - <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_address.html", library: library, index: library_index %> -
+ <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_address.html", index: library_index %> +
<% end %> \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex index e86bb67efad3..914518876a72 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex @@ -1,10 +1,10 @@ <% library_address = "library" <> to_string(@index) <> "_address" |> String.to_atom() %>
-
- <%= label :external_libraries, @library, gettext("Library") <> " " <> to_string(@index) <> " " <> gettext("Address") %> +
+ <%= label :external_libraries, library_address, gettext("Library") <> " " <> to_string(@index) <> " " <> gettext("Address") %>
- <%= text_input :external_libraries, library_address, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block" %> + <%= text_input :external_libraries, library_address, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block" %>
<%= if assigns[:tooltip_text] do @tooltip_text end %>
-
+
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex index 995d86cf9cc0..adb38c4d7c16 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex @@ -1,13 +1,11 @@
- <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_name.html", - library: :library1, - index: 1, - tooltip_text: gettext("A library name called in the .sol file. Multiple libraries (up to ") <> to_string(Application.get_env(:block_scout_web, :verification_max_libraries)) <> gettext(") may be added for each contract. Click the Add Library button to add an additional one.") - %> + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_name.html", + index: 1, + tooltip_text: gettext("A library name called in the .sol file. Multiple libraries (up to ") <> to_string(Application.get_env(:block_scout_web, :verification_max_libraries)) <> gettext(") may be added for each contract. Click the Add Library button to add an additional one.") + %> - <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_address.html", - library: :library1, - index: 1, - tooltip_text: gettext "The 0x library address. This can be found in the generated json file or Truffle output (if using truffle)." - %> + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_library_address.html", + index: 1, + tooltip_text: gettext "The 0x library address. This can be found in the generated json file or Truffle output (if using truffle)." + %>
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex index b1e0a7a5e0a3..fc15114a6294 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex @@ -1,10 +1,10 @@ <% library_name = "library" <> to_string(@index) <> "_name" |> String.to_atom() %>
-
- <%= label :external_libraries, @library, gettext("Library") <> " " <> to_string(@index) <> " " <> gettext("Name") %> -
- <%= text_input :external_libraries, library_name, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block" %> -
-
<%= if assigns[:tooltip_text] do @tooltip_text end %>
-
+
+ <%= label :external_libraries, library_name, gettext("Library") <> " " <> to_string(@index) <> " " <> gettext("Name") %> +
+ <%= text_input :external_libraries, library_name, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block" %> +
+
<%= if assigns[:tooltip_text] do @tooltip_text end %>
+
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex new file mode 100644 index 000000000000..8dd4bdebf564 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex @@ -0,0 +1,21 @@ +
+
+ <%= label @f, :is_yul, gettext("Is Yul contract") %> +
+
+
+ <%= radio_button @f, :is_yul, false, class: "form-check-input autodetectfalse" %> +
+ <%= label @f, :is_yul_false, gettext("No"), class: "radio-text" %> +
+
+ <%= radio_button @f, :is_yul, true, class: "form-check-input autodetecttrue", "aria-describedby": "is_yul-help-block" %> +
+ <%= label @f, :is_yul_true, gettext("Yes"), class: "radio-text" %> +
+
+ <%= error_tag @f, :is_yul, id: "is_yul-help-block", class: "text-danger form-error" %> +
+
<%= gettext("Select Yes if you want to vefify Yul contract.") %>
+
+
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 77285371dc1a..383aa4584d4c 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,6 +1,6 @@ <% metadata_for_verification = if assigns[:retrying], do: nil, else: Chain.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 %> +<% 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" %>
<%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> @@ -15,6 +15,10 @@ <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_address_field.html", f: f %> + <%= if RustVerifierInterface.enabled?() do %> + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_yul_contracts_switcher.html", f: f %> + <% end %> + <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_contract_name_field.html", f: f, tooltip: gettext "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name." %> <%= render BlockScoutWeb.AddressContractVerificationCommonFieldsView, "_include_nightly_builds_field.html", f: f %> @@ -23,7 +27,7 @@
- <%= label :evm_version, :evm_version, gettext("EVM Version") %> + <%= label f, :evm_version, gettext("EVM Version") %>
<%= select f, :evm_version, @evm_versions, class: "form-control border-rounded", "aria-describedby": "evm-version-help-block" %>
@@ -33,7 +37,7 @@
- <%= label f, "Optimization" %> + <%= label f, :optimization, gettext("Optimization") %>
@@ -55,7 +59,7 @@
">
- <%= label f, :name, gettext("Optimization runs") %> + <%= label f, :optimization_runs, gettext("Optimization runs") %>
<%= text_input f, :optimization_runs, class: "form-control border-rounded", "aria-describedby": "optimization-runs-help-block", "data-test": "optimization-runs" %>
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 1cee4b5c11cd..2b2950397e8e 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 @@ -4,7 +4,7 @@ <%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %>
-

<%= gettext "New Solidity Smart Contract Verification" %>

+

<%= if RustVerifierInterface.enabled?(), do: gettext "New Solidity/Yul Smart Contract Verification", else: gettext "New Solidity Smart Contract Verification" %>

<%= form_for changeset, address_contract_verification_path(@conn, :create), @@ -19,7 +19,7 @@
- <%= label :evm_version, :evm_version, gettext("EVM Version") %> + <%= label f, :evm_version, gettext("EVM Version") %>
<%= select f, :evm_version, @evm_versions, class: "form-control border-rounded", "aria-describedby": "evm-version-help-block" %>
@@ -29,18 +29,18 @@
- <%= label f, "Optimization" %> + <%= label f, :optimization, gettext("Optimization") %>
<%= radio_button f, :optimization, false, class: "form-check-input optimization-false" %>
- <%= label :smart_contract_optimization, :false, gettext("No"), class: "radio-text" %> + <%= label f, :optimization_false, gettext("No"), class: "radio-text" %>
<%= radio_button f, :optimization, true, class: "form-check-input optimization-true", "aria-describedby": "optimization-help-block" %>
- <%= label :smart_contract_optimization, :true, gettext("Yes"), class: "radio-text" %> + <%= label f, :optimization_true, gettext("Yes"), class: "radio-text" %>
<%= error_tag f, :optimization, id: "optimization-help-block", class: "text-danger form-error" %> @@ -51,7 +51,7 @@
">
- <%= label f, :name, gettext("Optimization runs") %> + <%= label f, :optimization_runs, gettext("Optimization runs") %>
<%= text_input f, :optimization_runs, class: "form-control border-rounded", "aria-describedby": "optimization-runs-help-block", "data-test": "optimization-runs" %>
@@ -60,7 +60,7 @@
- +
@@ -69,7 +69,7 @@
-
<%= gettext "Drop all Solidity contract source files into the drop zone." %>
+
<%= if RustVerifierInterface.enabled?(), do: gettext "Drop all Solidity or Yul contract source files into the drop zone.", else: gettext "Drop all Solidity contract source files into the drop zone." %>
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 e86c15880e60..9eb3c96ea58a 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,6 +1,6 @@ <% metadata_for_verification = Chain.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 %> +<% 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" %>
<%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex index 469380ca6e82..4cb36fc3a73f 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex @@ -62,5 +62,4 @@ <% end %>
-
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex index 180d51d9d04d..c2655c48ecf7 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex @@ -1,7 +1,6 @@ - <% coin_or_transfer = if @coin_or_token_transfers == :coin, do: :coin, else: elem(List.first(@coin_or_token_transfers), 1)%> <%= if coin_or_transfer != :coin and coin_or_transfer.token.type != "ERC-20" or has_diff?(@balance_diff) do %> - + <%= if @address.hash == @burn_address_hash do %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex new file mode 100644 index 000000000000..b78d3ef514ec --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex @@ -0,0 +1,36 @@ +
+
+
+
+
<%= gettext("UML diagram") %>
+

+ <%= gettext("For contract") %> + <%= link to: address_contract_path(@conn, :index, @address), "data-test": "address_hash_link" do %> + <%= render( + BlockScoutWeb.AddressView, + "_responsive_hash.html", + address: @address, + contract: true, + use_custom_tooltip: false + ) %> + <% end %> +

+
+
+ + + + + +
+ + + +
+
+
+
+ +
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 3a120c597f19..f6e676d7fcdf 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 @@ -3,4 +3,5 @@ defmodule BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView do 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 7ec8e9b7d989..12a80e5eb282 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 @@ -3,4 +3,5 @@ defmodule BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView do 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_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex index 8856d14c3045..ce7b4dc00ba1 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 @@ -130,7 +130,7 @@ defmodule BlockScoutWeb.AddressContractView do end def creation_code(%Address{contracts_creation_internal_transaction: %InternalTransaction{}} = address) do - address.contracts_creation_internal_transaction.input + address.contracts_creation_internal_transaction.init end def creation_code(%Address{contracts_creation_transaction: %Transaction{}} = address) do 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 f06c97e33443..e08ee9836b44 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 @@ -242,9 +242,7 @@ defmodule BlockScoutWeb.AddressView do @doc """ Returns the primary name of an address if available. If there is no names on address function performs preload of names association. """ - def primary_name(_, second_time? \\ false) - - def primary_name(%Address{names: [_ | _] = address_names}, _second_time?) do + def primary_name(%Address{names: [_ | _] = address_names}) do case Enum.find(address_names, &(&1.primary == true)) do nil -> %Address.Name{name: name} = Enum.at(address_names, 0) @@ -255,11 +253,20 @@ defmodule BlockScoutWeb.AddressView do end end - def primary_name(%Address{names: _} = address, false) do - primary_name(Repo.preload(address, [:names]), true) + def primary_name(%Address{names: %Ecto.Association.NotLoaded{}} = address) do + primary_name(Repo.preload(address, [:names])) end - def primary_name(%Address{names: _}, true), do: nil + def primary_name(%Address{names: _} = address) do + with false <- is_nil(address.contract_code), + twin <- Chain.get_verified_twin_contract(address), + false <- is_nil(twin) do + twin.name + else + _ -> + nil + end + end def implementation_name(%Address{smart_contract: %{implementation_name: implementation_name}}), do: implementation_name @@ -308,22 +315,22 @@ 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 || [], &is_read_function?(&1)) end def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false def is_read_function?(function), do: Helper.queriable_method?(function) || Helper.read_with_wallet_method?(function) - def smart_contract_is_proxy?(%Address{smart_contract: %SmartContract{}} = address) do - Chain.proxy_contract?(address.hash, address.smart_contract.abi) + def smart_contract_is_proxy?(%Address{smart_contract: %SmartContract{} = smart_contract}) do + SmartContract.proxy_contract?(smart_contract) end def smart_contract_is_proxy?(%Address{smart_contract: nil}), do: false def smart_contract_with_write_functions?(%Address{smart_contract: %SmartContract{}} = address) do Enum.any?( - address.smart_contract.abi, + address.smart_contract.abi || [], &Writer.write_function?(&1) ) end 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 ace6af7517b1..d1686cef1565 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 @@ -52,6 +52,18 @@ defmodule BlockScoutWeb.API.RPC.ContractView do |> set_external_libraries(contract) |> set_verified_contract_data(contract, address, optimization) |> set_proxy_info(contract) + |> set_compiler_settings(contract) + end + + defp set_compiler_settings(contract_output, contract) when contract == %{}, do: contract_output + + defp set_compiler_settings(contract_output, contract) do + if is_nil(contract.compiler_settings) do + contract_output + else + contract_output + |> Map.put(:CompilerSettings, contract.compiler_settings) + end end defp set_proxy_info(contract_output, contract) when contract == %{} do 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 54511abc0027..6d43e493e195 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 @@ -1,8 +1,12 @@ defmodule BlockScoutWeb.API.V2.AddressView do use BlockScoutWeb, :view + alias BlockScoutWeb.AddressView alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView} alias BlockScoutWeb.API.V2.Helper + alias Explorer.{Chain, Market} + alias Explorer.Chain.{Address, SmartContract} + alias Explorer.ExchangeRates.Token def render("message.json", assigns) do ApiView.render("message.json", assigns) @@ -29,7 +33,38 @@ defmodule BlockScoutWeb.API.V2.AddressView do end def prepare_address(address, conn \\ nil) do - Helper.address_with_info(conn, address, address.hash) + base_info = Helper.address_with_info(conn, address, address.hash) + is_proxy = AddressView.smart_contract_is_proxy?(address) + + {implementation_address, implementation_name} = + with true <- is_proxy, + {address, name} <- SmartContract.get_implementation_address_hash(address.smart_contract), + false <- is_nil(address), + {:ok, address_hash} <- Chain.string_to_address_hash(address), + checksummed_address <- Address.checksum(address_hash) do + {checksummed_address, name} + else + _ -> + {nil, nil} + end + + balance = address.fetched_coin_balance && address.fetched_coin_balance.value + exchange_rate = (Market.get_exchange_rate(Explorer.coin()) || Token.null()).usd_value + + creator_hash = AddressView.from_address_hash(address) + creation_tx = creator_hash && AddressView.transaction_hash(address) + token = address.token && TokenView.render("token.json", %{token: Market.add_price(address.token)}) + + Map.merge(base_info, %{ + "creator_address_hash" => creator_hash && Address.checksum(creator_hash), + "creation_tx_hash" => creation_tx, + "token" => token, + "coin_balance" => balance, + "exchange_rate" => exchange_rate, + "implementation_name" => implementation_name, + "implementation_address" => implementation_address, + "block_number_balance_updated_at" => address.fetched_coin_balance_block_number + }) end def prepare_token_balance({token_balance, token}) do 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 57f914bc0ce1..6dc1808e318b 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 @@ -10,6 +10,10 @@ defmodule BlockScoutWeb.API.V2.Helper do import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2, get_tags_on_address: 1] + def address_with_info(_, _, nil) do + nil + end + def address_with_info(conn, address, address_hash) do %{ personal_tags: private_tags, @@ -27,7 +31,7 @@ defmodule BlockScoutWeb.API.V2.Helper do def address_with_info(%Address{} = address, _address_hash) do %{ - "hash" => to_string(address), + "hash" => Address.checksum(address), "is_contract" => is_smart_contract(address), "name" => address_name(address), "implementation_name" => implementation_name(address), @@ -39,8 +43,18 @@ defmodule BlockScoutWeb.API.V2.Helper do address_with_info(nil, address_hash) end + def address_with_info(nil, nil) do + nil + end + def address_with_info(nil, address_hash) do - %{"hash" => address_hash, "is_contract" => false, "name" => nil, "implementation_name" => nil, "is_verified" => nil} + %{ + "hash" => Address.checksum(address_hash), + "is_contract" => false, + "name" => nil, + "implementation_name" => nil, + "is_verified" => nil + } end def address_name(%Address{names: [_ | _] = address_names}) do 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 e0bdae9c989f..66ae64783ffe 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,13 +1,41 @@ defmodule BlockScoutWeb.API.V2.TokenView do + alias BlockScoutWeb.API.V2.Helper + alias Explorer.Chain.Address + def render("token.json", %{token: token}) do %{ - "address" => token.contract_address_hash, + "address" => Address.checksum(token.contract_address_hash), "symbol" => token.symbol, "name" => token.name, "decimals" => token.decimals, "type" => token.type, - "holders" => to_string(token.holder_count), - "exchange_rate" => token.usd_value && to_string(token.usd_value) + "holders" => token.holder_count && to_string(token.holder_count), + "exchange_rate" => exchange_rate(token), + "total_supply" => token.total_supply + } + end + + def render("token_balances.json", %{ + token_balances: token_balances, + next_page_params: next_page_params, + conn: conn, + token: token + }) do + %{ + "items" => Enum.map(token_balances, &prepare_token_balance(&1, conn, token)), + "next_page_params" => next_page_params + } + end + + def exchange_rate(%{usd_value: usd_value}) when not is_nil(usd_value), do: to_string(usd_value) + def exchange_rate(_), do: nil + + def prepare_token_balance(token_balance, conn, token) do + %{ + "address" => Helper.address_with_info(conn, token_balance.address, token_balance.address_hash), + "value" => token_balance.value, + "token_id" => token_balance.token_id, + "token" => render("token.json", %{token: token}) } 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 c2f85b2ba62f..651547ba7f3c 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 @@ -43,8 +43,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do %{"method_id" => method_id, "method_call" => text, "parameters" => prepare_method_mapping(mapping)} end - def render("revert_reason.json", %{raw: raw, decoded: decoded}) do - %{"raw" => raw, "decoded" => decoded} + def render("revert_reason.json", %{raw: raw}) do + %{"raw" => raw} end def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do @@ -88,7 +88,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "to" => Helper.address_with_info(conn, token_transfer.to_address, token_transfer.to_address_hash), "total" => prepare_token_transfer_total(token_transfer), "token" => TokenView.render("token.json", %{token: Market.add_price(token_transfer.token)}), - "type" => Chain.get_token_transfer_type(token_transfer) + "type" => Chain.get_token_transfer_type(token_transfer), + "timestamp" => block_timestamp(token_transfer.block) } end @@ -204,7 +205,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "result" => status, "status" => transaction.status, "block" => transaction.block_number, - "timestamp" => transaction.block && transaction.block.timestamp, + "timestamp" => block_timestamp(transaction.block), "from" => Helper.address_with_info(conn, transaction.from_address, transaction.from_address_hash), "to" => Helper.address_with_info(conn, transaction.to_address, transaction.to_address_hash), "created_contract" => @@ -286,8 +287,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do _ -> hex = TransactionView.get_pure_transaction_revert_reason(transaction) - utf8 = TransactionView.decoded_revert_reason(transaction) - render(__MODULE__, "revert_reason.json", raw: hex, decoded: utf8) + render(__MODULE__, "revert_reason.json", raw: hex) end end end @@ -444,4 +444,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do types end end + + defp block_timestamp(%Block{} = block), do: block.timestamp + defp block_timestamp(_), do: nil end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex index 4a25a937fdc8..1c0c748dc415 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex @@ -122,7 +122,7 @@ defmodule BlockScoutWeb.Tokens.Instance.OverviewView do def smart_contract_with_read_only_functions?( %Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token ) do - Enum.any?(token.contract_address.smart_contract.abi, &Helper.queriable_method?(&1)) + Enum.any?(token.contract_address.smart_contract.abi || [], &Helper.queriable_method?(&1)) end def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false 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 e258938f1b95..4fc1c6d28ef4 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 @@ -48,13 +48,13 @@ defmodule BlockScoutWeb.Tokens.OverviewView do def smart_contract_with_read_only_functions?( %Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token ) do - Enum.any?(token.contract_address.smart_contract.abi, &Helper.queriable_method?(&1)) + Enum.any?(token.contract_address.smart_contract.abi || [], &Helper.queriable_method?(&1)) end 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{}} = address}) do - Chain.proxy_contract?(address.hash, address.smart_contract.abi) + def smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: %SmartContract{} = smart_contract}}) do + SmartContract.proxy_contract?(smart_contract) end def smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: nil}}), do: false @@ -63,7 +63,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do contract_address: %Address{smart_contract: %SmartContract{}} = address }) do Enum.any?( - address.smart_contract.abi, + address.smart_contract.abi || [], &Writer.write_function?(&1) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex b/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex new file mode 100644 index 000000000000..827deeeecdec --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.VisualizeSol2umlView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex index 88a5fb2f8478..db5dc981e0c7 100644 --- a/apps/block_scout_web/lib/block_scout_web/web_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex @@ -529,6 +529,8 @@ defmodule BlockScoutWeb.WebRouter do get("/makerdojo", MakerdojoController, :index) + get("/visualize/sol2uml", VisualizeSol2umlController, :index) + get("/*path", PageNotFoundController, :index) end end 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 new file mode 100644 index 000000000000..33fbe54aee77 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -0,0 +1,1207 @@ +defmodule BlockScoutWeb.API.V2.AddressControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.{Chain, Repo} + + alias Explorer.Chain.{ + Address, + Address.CoinBalance, + Block, + InternalTransaction, + Log, + Token, + TokenTransfer, + Transaction + } + + alias Explorer.Chain.Address.CurrentTokenBalance + + describe "/addresses/{address_hash}" do + test "get 404 on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get address & get the same response for checksummed and downcased parameter", %{conn: conn} do + address = insert(:address) + + correct_reponse = %{ + "hash" => Address.checksum(address.hash), + "implementation_name" => nil, + "is_contract" => false, + "is_verified" => false, + "name" => nil, + "private_tags" => [], + "public_tags" => [], + "watchlist_names" => [], + "creator_address_hash" => nil, + "creation_tx_hash" => nil, + "token" => nil, + "coin_balance" => nil, + "exchange_rate" => nil, + "implementation_name" => nil, + "implementation_address" => nil, + "block_number_balance_updated_at" => nil + } + + request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") + assert ^correct_reponse = json_response(request, 200) + + request = get(conn, "/api/v2/addresses/#{String.downcase(to_string(address.hash))}") + assert ^correct_reponse = json_response(request, 200) + end + end + + describe "/addresses/{address_hash}/counters" do + test "get 404 on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/counters") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get counters with 0s", %{conn: conn} do + address = insert(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + assert %{ + "transactions_count" => "0", + "token_transfers_count" => "0", + "gas_usage_count" => "0", + "validations_count" => "0" + } = json_response(request, 200) + end + + test "get counters", %{conn: conn} do + address = insert(:address) + + tx_from = insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + another_tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: another_tx, + block: another_tx.block, + block_number: another_tx.block_number + ) + + insert(:token_transfer, + to_address: address, + transaction: another_tx, + block: another_tx.block, + block_number: another_tx.block_number + ) + + insert(:block, miner: address) + + Chain.transaction_count(address) + Chain.token_transfers_count(address) + Chain.gas_usage_count(address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + + gas_used = to_string(tx_from.gas_used) + + assert %{ + "transactions_count" => "2", + "token_transfers_count" => "2", + "gas_usage_count" => ^gas_used, + "validations_count" => "1" + } = json_response(request, 200) + end + end + + describe "/addresses/{address_hash}/transactions" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant transaction", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction, from_address: address) |> with_block() + + insert(:transaction) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(tx, Enum.at(response["items"], 0)) + end + + test "get pending transaction", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction, from_address: address) |> with_block() + pending_tx = insert(:transaction, from_address: address) + + insert(:transaction) |> with_block() + insert(:transaction) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + compare_item(pending_tx, Enum.at(response["items"], 0)) + compare_item(tx, Enum.at(response["items"], 1)) + end + + test "get only :to transaction", %{conn: conn} do + address = insert(:address) + + insert(:transaction, from_address: address) |> with_block() + tx = insert(:transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "to"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(tx, Enum.at(response["items"], 0)) + end + + test "get only :from transactions", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction, from_address: address) |> with_block() + insert(:transaction, to_address: address) |> with_block() + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "from"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(tx, Enum.at(response["items"], 0)) + end + + test "validated txs can paginate", %{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") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + end + + test "pending txs can paginate", %{conn: conn} do + address = insert(:address) + + txs = insert_list(51, :transaction, from_address: address) + + 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", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + end + + test "pending + validated txs can paginate", %{conn: conn} do + address = insert(:address) + + txs_pending = insert_list(51, :transaction, from_address: address) + txs_validated = insert_list(50, :transaction, to_address: address) |> with_block() + + 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", 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_pending, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs_pending, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(txs_pending, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(txs_validated, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(txs_validated, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, txs_validated ++ [Enum.at(txs_pending, 0)]) + end + + test ":to txs can paginate", %{conn: conn} do + address = insert(:address) + + txs = insert_list(51, :transaction, to_address: address) |> with_block() + insert_list(51, :transaction, from_address: address) |> with_block() + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/transactions", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + end + + test ":from txs can paginate", %{conn: conn} do + address = insert(:address) + + insert_list(51, :transaction, to_address: address) |> with_block() + txs = insert_list(51, :transaction, from_address: address) |> with_block() + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/transactions", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + end + + test ":from + :to txs can paginate", %{conn: conn} do + address = insert(:address) + + txs_from = insert_list(50, :transaction, from_address: address) |> with_block() + txs_to = insert_list(51, :transaction, to_address: address) |> with_block() + + 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", 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_to, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(txs_to, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(txs_to, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(txs_from, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(txs_from, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, txs_from ++ [Enum.at(txs_to, 0)]) + end + end + + describe "/addresses/{address_hash}/token-transfers" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/token-transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant token transfer", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number) + + token_transfer = + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "get only :to token transfer", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + + token_transfer = + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "to"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "get only :from token transfer", %{conn: conn} do + address = insert(:address) + + tx = insert(:transaction) |> with_block() + + token_transfer = + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "from"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "token transfers can paginate", %{conn: conn} do + address = insert(:address) + + token_tranfers = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_tranfers) + end + + test ":to token transfers can paginate", %{conn: conn} do + address = insert(:address) + + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + token_tranfers = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + end + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", 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_tranfers) + end + + test ":from token transfers can paginate", %{conn: conn} do + address = insert(:address) + + token_tranfers = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + end + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", 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_tranfers) + end + + test ":from + :to tt can paginate", %{conn: conn} do + address = insert(:address) + + tt_from = + for _ <- 0..49 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address) + end + + tt_to = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", 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(tt_to, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(tt_to, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(tt_to, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(tt_from, 49), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(tt_from, 1), Enum.at(response_2nd_page["items"], 49)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response_2nd_page["next_page_params"]) + assert response = json_response(request, 200) + + check_paginated_response(response_2nd_page, response, tt_from ++ [Enum.at(tt_to, 0)]) + end + + test "check token type filters", %{conn: conn} do + address = insert(:address) + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_20_token.contract_address + ) + end + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + tx = insert(:transaction) |> 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 + + erc_1155_token = insert(:token, type: "ERC-1155") + + erc_1155_tt = + for x <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_1155_token.contract_address, + token_ids: [x] + ) + end + + # -- ERC-20 -- + filter = %{"type" => "ERC-20"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_20_tt) + # -- ------ -- + + # -- ERC-721 -- + filter = %{"type" => "ERC-721"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + # -- ------ -- + + # -- ERC-1155 -- + filter = %{"type" => "ERC-1155"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_1155_tt) + # -- ------ -- + + # two filters simultaneously + filter = %{"type" => "ERC-1155,ERC-20"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + 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(erc_1155_tt, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(erc_1155_tt, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(erc_1155_tt, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(erc_20_tt, 2), Enum.at(response_2nd_page["items"], 49)) + + request_3rd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/token-transfers", + Map.merge(response_2nd_page["next_page_params"], filter) + ) + + assert response_3rd_page = json_response(request_3rd_page, 200) + assert Enum.count(response_3rd_page["items"]) == 2 + assert response_3rd_page["next_page_params"] == nil + compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1)) + # -- ------ -- + end + + test "type and direction filters at the same time", %{conn: conn} do + address = insert(:address) + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + from_address: address, + token_contract_address: erc_20_token.contract_address + ) + end + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + to_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + + filter = %{"type" => "ERC-721", "filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + filter = %{"type" => "ERC-721", "filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + + filter = %{"type" => "ERC-721,ERC-20", "filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + + filter = %{"type" => "ERC-721,ERC-20", "filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_20_tt) + end + end + + describe "/addresses/{address_hash}/internal-transactions" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/internal-transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get internal tx and filter working", %{conn: conn} do + address = insert(:address) + + tx = + :transaction + |> insert() + |> with_block() + + internal_tx_from = + insert(:internal_transaction, + transaction: tx, + index: 1, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: 1, + from_address: address + ) + + internal_tx_to = + insert(:internal_transaction, + transaction: tx, + index: 2, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: 2, + to_address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + + compare_item(internal_tx_from, Enum.at(response["items"], 1)) + compare_item(internal_tx_to, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "from"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_tx_from, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "to"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_tx_to, Enum.at(response["items"], 0)) + end + + test "internal txs can paginate", %{conn: conn} do + address = insert(:address) + + tx = + :transaction + |> insert() + |> with_block() + + itxs_from = + for i <- 1..51 do + insert(:internal_transaction, + transaction: tx, + index: i, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: i, + from_address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, itxs_from) + + itxs_to = + for i <- 52..102 do + insert(:internal_transaction, + transaction: tx, + index: i, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: i, + to_address: address + ) + end + + filter = %{"filter" => "to"} + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/internal-transactions", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, itxs_to) + + filter = %{"filter" => "from"} + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/addresses/#{address.hash}/internal-transactions", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, itxs_from) + end + end + + describe "/addresses/{address_hash}/blocks-validated" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/blocks-validated") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get relevant block validated", %{conn: conn} do + address = insert(:address) + insert(:block) + block = insert(:block, miner: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + compare_item(block, Enum.at(response["items"], 0)) + end + + test "blocks validated can be paginated", %{conn: conn} do + address = insert(:address) + insert(:block) + blocks = insert_list(51, :block, miner: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, blocks) + end + end + + describe "/addresses/{address_hash}/token-balances" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") + + assert response = json_response(request, 200) + assert response == [] + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/token-balances") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token balance", %{conn: conn} do + address = insert(:address) + + ctbs = + for _ <- 0..50 do + insert(:address_current_token_balance_with_token_id, address: address) |> Repo.preload([:token]) + end + |> Enum.sort_by(fn x -> x.value end, :desc) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") + + assert response = json_response(request, 200) + + for i <- 0..50 do + compare_item(Enum.at(ctbs, i), Enum.at(response, i)) + end + end + end + + describe "/addresses/{address_hash}/coin-balance-history" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/coin-balance-history") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get coin balance history", %{conn: conn} do + address = insert(:address) + + insert(:address_coin_balance) + acb = insert(:address_coin_balance, address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + compare_item(acb, Enum.at(response["items"], 0)) + end + + test "coin balance history can paginate", %{conn: conn} do + address = insert(:address) + + acbs = insert_list(51, :address_coin_balance, address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, acbs) + end + end + + describe "/addresses/{address_hash}/coin-balance-history-by-day" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day") + + assert response = json_response(request, 200) + assert response == [] + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/coin-balance-history-by-day") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get coin balance history by day", %{conn: conn} do + address = insert(:address) + noon = Timex.now() |> Timex.beginning_of_day() |> Timex.set(hour: 12) + block = insert(:block, timestamp: noon, number: 2) + block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1), number: 1) + insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block.number) + insert(:fetched_balance, address_hash: address.hash, value: 2000, block_number: block_one_day_ago.number) + insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: noon) + insert(:fetched_balance_daily, address_hash: address.hash, value: 2000, day: Timex.shift(noon, days: -1)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day") + + response = json_response(request, 200) + + assert [ + %{"date" => _, "value" => "2000"}, + %{"date" => _, "value" => "1000"}, + %{"date" => _, "value" => "1000"} + ] = response + end + end + + describe "/addresses/{address_hash}/logs" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/logs") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get log", %{conn: conn} do + address = insert(:address) + + tx = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: tx, + index: 1, + block: tx.block, + block_number: tx.block_number, + address: address + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + end + + # for some reasons test does not work if run as single test + test "logs can paginate", %{conn: conn} do + address = insert(:address) + + logs = + for x <- 0..50 do + tx = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: tx, + index: x, + block: tx.block, + block_number: tx.block_number, + address: address + ) + end + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/logs", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + check_paginated_response(response, response_2nd_page, logs) + end + + test "logs can be filtered by topic", %{conn: conn} do + address = insert(:address) + + for x <- 0..20 do + tx = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: tx, + index: x, + block: tx.block, + block_number: tx.block_number, + address: address + ) + end + + tx = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + address: address, + first_topic: "0x123456789123456789" + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs?topic=0x123456789123456789") + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + end + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["tx_hash"] + end + + defp compare_item(%InternalTransaction{} = internal_tx, json) do + assert internal_tx.block_number == json["block"] + assert to_string(internal_tx.gas) == json["gas_limit"] + assert internal_tx.index == json["index"] + assert to_string(internal_tx.transaction_hash) == json["transaction_hash"] + assert Address.checksum(internal_tx.from_address_hash) == json["from"]["hash"] + assert Address.checksum(internal_tx.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%Block{} = block, json) do + assert to_string(block.hash) == json["hash"] + assert block.number == json["height"] + end + + defp compare_item(%CurrentTokenBalance{} = ctb, json) do + assert to_string(ctb.value) == json["value"] + assert (ctb.token_id && to_string(ctb.token_id)) == json["token_id"] + compare_item(ctb.token, json["token"]) + end + + defp compare_item(%CoinBalance{} = cb, json) do + assert to_string(cb.value.value) == json["value"] + assert cb.block_number == json["block_number"] + + assert Jason.encode!(Repo.get_by(Block, number: cb.block_number).timestamp) =~ + String.replace(json["block_timestamp"], "Z", "") + end + + defp compare_item(%Token{} = token, json) do + assert Address.checksum(token.contract_address_hash) == json["address"] + assert to_string(token.symbol) == json["symbol"] + assert to_string(token.name) == json["name"] + assert to_string(token.type) == json["type"] + assert to_string(token.decimals) == json["decimals"] + assert (token.holder_count && to_string(token.holder_count)) == json["holders"] + assert Map.has_key?(json, "exchange_rate") + end + + defp compare_item(%Log{} = log, json) do + assert to_string(log.data) == json["data"] + assert log.index == json["index"] + assert Address.checksum(log.address_hash) == json["address"]["hash"] + 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 +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs new file mode 100644 index 000000000000..d16e457441bf --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -0,0 +1,329 @@ +defmodule BlockScoutWeb.API.V2.BlockControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, Block, Transaction} + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id()) + + :ok + end + + describe "/blocks" do + test "empty lists", %{conn: conn} do + request = get(conn, "/api/v2/blocks") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks", %{"type" => "uncle"}) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks", %{"type" => "reorg"}) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks", %{"type" => "block"}) + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get block", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(block, Enum.at(response["items"], 0)) + end + + test "type=block returns only consensus blocks", %{conn: conn} do + blocks = + 4 + |> insert_list(:block) + |> Enum.reverse() + + for index <- 0..3 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + end + + 2 + |> insert_list(:block, consensus: false) + + request = get(conn, "/api/v2/blocks", %{"type" => "block"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + + for index <- 0..3 do + compare_item(Enum.at(blocks, index), Enum.at(response["items"], index)) + end + end + + test "type=block can paginate", %{conn: conn} do + blocks = + 51 + |> insert_list(:block) + + filter = %{"type" => "block"} + + request = get(conn, "/api/v2/blocks", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, blocks) + end + + test "type=reorg returns only non consensus blocks", %{conn: conn} do + blocks = + 5 + |> insert_list(:block) + + for index <- 0..3 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + end + + reorgs = + 4 + |> insert_list(:block, consensus: false) + |> Enum.reverse() + + request = get(conn, "/api/v2/blocks", %{"type" => "reorg"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + + for index <- 0..3 do + compare_item(Enum.at(reorgs, index), Enum.at(response["items"], index)) + end + end + + test "type=reorg can paginate", %{conn: conn} do + reorgs = + 51 + |> insert_list(:block, consensus: false) + + filter = %{"type" => "reorg"} + request = get(conn, "/api/v2/blocks", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, reorgs) + end + + test "type=uncle returns only uncle blocks", %{conn: conn} do + blocks = + 4 + |> insert_list(:block) + |> Enum.reverse() + + uncles = + for index <- 0..3 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + uncle + end + |> Enum.reverse() + + 4 + |> insert_list(:block, consensus: false) + + request = get(conn, "/api/v2/blocks", %{"type" => "uncle"}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 4 + assert response["next_page_params"] == nil + + for index <- 0..3 do + compare_item(Enum.at(uncles, index), Enum.at(response["items"], index)) + end + end + + test "type=uncle can paginate", %{conn: conn} do + blocks = + 51 + |> insert_list(:block) + + uncles = + for index <- 0..50 do + uncle = insert(:block, consensus: false) + insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index)) + uncle + end + + filter = %{"type" => "uncle"} + request = get(conn, "/api/v2/blocks", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter)) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, uncles) + end + end + + describe "/blocks/{block_hash_or_number}" do + test "return 422 on invalid parameter", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) + + request_2 = get(conn, "/api/v2/blocks/123qwe") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end + + test "return 404 on non existing block", %{conn: conn} do + block = build(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}") + assert %{"message" => "Not found"} = json_response(request_1, 404) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}") + assert %{"message" => "Not found"} = json_response(request_2, 404) + end + + test "get the same blocks by hash and number", %{conn: conn} do + block = insert(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}") + assert response_1 = json_response(request_1, 200) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}") + assert response_2 = json_response(request_2, 200) + + assert response_2 == response_1 + compare_item(block, response_2) + end + end + + describe "/blocks/{block_hash_or_number}/transactions" do + test "return 422 on invalid parameter", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123/transactions") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) + + request_2 = get(conn, "/api/v2/blocks/123qwe/transactions") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end + + test "return 404 on non existing block", %{conn: conn} do + block = build(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert %{"message" => "Not found"} = json_response(request_1, 404) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert %{"message" => "Not found"} = json_response(request_2, 404) + end + + test "get empty list", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get relevant tx", %{conn: conn} do + 10 + |> insert_list(:transaction) + |> with_block() + + block = insert(:block) + + tx = + :transaction + |> insert() + |> with_block(block) + + request = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(tx, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert response_1 = json_response(request, 200) + assert response_1 == response + end + + test "get txs with working next_page_params", %{conn: conn} do + 2 + |> insert_list(:transaction) + |> with_block() + + block = insert(:block) + + txs = + 51 + |> insert_list(:transaction) + |> with_block(block) + |> Enum.reverse() + + request = get(conn, "/api/v2/blocks/#{block.number}/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks/#{block.number}/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + + request_1 = get(conn, "/api/v2/blocks/#{block.hash}/transactions") + assert response_1 = json_response(request_1, 200) + + assert response_1 == response + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions", response_1["next_page_params"]) + assert response_2 = json_response(request_2, 200) + assert response_2 == response_2nd_page + end + end + + defp compare_item(%Block{} = block, json) do + assert to_string(block.hash) == json["hash"] + assert block.number == json["height"] + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + 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 +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs new file mode 100644 index 000000000000..0c00722d58bb --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.API.V2.ConfigControllerTest do + use BlockScoutWeb.ConnCase + + describe "/config/json-rpc-url" do + test "get json rps url if set", %{conn: conn} do + url = "http://rps.url:1234/v1" + Application.put_env(:block_scout_web, :json_rpc, url) + + request = get(conn, "/api/v2/config/json-rpc-url") + + assert %{"json_rpc_url" => ^url} = json_response(request, 200) + end + + test "get empty json rps url if not set", %{conn: conn} do + Application.put_env(:block_scout_web, :json_rpc, nil) + + request = get(conn, "/api/v2/config/json-rpc-url") + + assert %{"json_rpc_url" => nil} = json_response(request, 200) + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs new file mode 100644 index 000000000000..b1e27673df8c --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs @@ -0,0 +1,77 @@ +defmodule BlockScoutWeb.API.V2.MainPageControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, Block, Transaction} + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + + :ok + end + + describe "/main-page/blocks" do + test "get empty list when no blocks", %{conn: conn} do + request = get(conn, "/api/v2/main-page/blocks") + assert [] = json_response(request, 200) + end + + test "get last 4 blocks", %{conn: conn} do + blocks = insert_list(10, :block) |> Enum.take(-4) |> Enum.reverse() + + request = get(conn, "/api/v2/main-page/blocks") + assert response = json_response(request, 200) + assert Enum.count(response) == 4 + + for i <- 0..3 do + compare_item(Enum.at(blocks, i), Enum.at(response, i)) + end + end + end + + describe "/main-page/transactions" do + test "get empty list when no txs", %{conn: conn} do + request = get(conn, "/api/v2/main-page/transactions") + assert [] = json_response(request, 200) + end + + test "get last 5 txs", %{conn: conn} do + txs = insert_list(10, :transaction) |> with_block() |> Enum.take(-6) |> Enum.reverse() + + request = get(conn, "/api/v2/main-page/transactions") + assert response = json_response(request, 200) + assert Enum.count(response) == 6 + + for i <- 0..5 do + compare_item(Enum.at(txs, i), Enum.at(response, i)) + end + end + end + + describe "/main-page/indexing-status" do + test "get indexing status", %{conn: conn} do + request = get(conn, "/api/v2/main-page/indexing-status") + assert request = json_response(request, 200) + + assert Map.has_key?(request, "finished_indexing_blocks") + assert Map.has_key?(request, "finished_indexing") + assert Map.has_key?(request, "indexed_blocks_ratio") + assert Map.has_key?(request, "indexed_inernal_transactions_ratio") + end + end + + defp compare_item(%Block{} = block, json) do + assert to_string(block.hash) == json["hash"] + assert block.number == json["height"] + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + 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 new file mode 100644 index 000000000000..211c68dc79f0 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -0,0 +1,147 @@ +defmodule BlockScoutWeb.API.V2.SearchControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.Address + + setup do + insert(:block) + insert(:unique_smart_contract) + insert(:unique_token) + insert(:transaction) + address = insert(:address) + insert(:unique_address_name, address: address) + + :ok + end + + describe "/search" do + test "search block", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/search?q=#{block.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "block" + assert item["block_number"] == block.number + assert item["block_hash"] == to_string(block.hash) + assert item["url"] =~ to_string(block.hash) + + request = get(conn, "/api/v2/search?q=#{block.number}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "block" + assert item["block_number"] == block.number + assert item["block_hash"] == to_string(block.hash) + assert item["url"] =~ to_string(block.hash) + end + + test "search address", %{conn: conn} do + address = insert(:address) + name = insert(:unique_address_name, address: address) + + request = get(conn, "/api/v2/search?q=#{address.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "address" + assert item["name"] == name.name + assert item["address"] == Address.checksum(address.hash) + assert item["url"] =~ Address.checksum(address.hash) + end + + test "search contract", %{conn: conn} do + contract = insert(:unique_smart_contract) + + request = get(conn, "/api/v2/search?q=#{contract.name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "contract" + assert item["name"] == contract.name + assert item["address"] == Address.checksum(contract.address_hash) + assert item["url"] =~ Address.checksum(contract.address_hash) + end + + test "check pagination", %{conn: conn} do + name = "contract" + contracts = insert_list(51, :smart_contract, name: name) + + request = get(conn, "/api/v2/search?q=#{name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "contract" + assert item["name"] == name + + request_2 = get(conn, "/api/v2/search", response["next_page_params"]) + assert response_2 = json_response(request_2, 200) + + assert Enum.count(response_2["items"]) == 1 + assert response_2["next_page_params"] == nil + + item = Enum.at(response_2["items"], 0) + + assert item["type"] == "contract" + assert item["name"] == name + + assert item not in response["items"] + end + + test "search token", %{conn: conn} do + token = insert(:unique_token) + + request = get(conn, "/api/v2/search?q=#{token.name}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + 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) + end + + test "search transaction", %{conn: conn} do + tx = insert(:transaction) + + request = get(conn, "/api/v2/search?q=#{tx.hash}") + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + item = Enum.at(response["items"], 0) + + assert item["type"] == "transaction" + assert item["tx_hash"] == to_string(tx.hash) + assert item["url"] =~ to_string(tx.hash) + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs new file mode 100644 index 000000000000..803d5ad51ca3 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs @@ -0,0 +1,57 @@ +defmodule BlockScoutWeb.API.V2.StatsControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Counters.{AddressesCounter, AverageBlockTime} + + describe "/stats" do + setup do + start_supervised!(AddressesCounter) + start_supervised!(AverageBlockTime) + + Application.put_env(:explorer, AverageBlockTime, enabled: true) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false) + end) + + :ok + end + + test "get all fields", %{conn: conn} do + request = get(conn, "/api/v2/stats") + assert response = json_response(request, 200) + + assert Map.has_key?(response, "total_blocks") + assert Map.has_key?(response, "total_addresses") + assert Map.has_key?(response, "total_transactions") + assert Map.has_key?(response, "average_block_time") + assert Map.has_key?(response, "coin_price") + assert Map.has_key?(response, "total_gas_used") + assert Map.has_key?(response, "transactions_today") + assert Map.has_key?(response, "gas_used_today") + assert Map.has_key?(response, "gas_prices") + assert Map.has_key?(response, "static_gas_price") + assert Map.has_key?(response, "market_cap") + assert Map.has_key?(response, "network_utilization_percentage") + end + end + + describe "/stats/charts/market" do + test "get empty data", %{conn: conn} do + request = get(conn, "/api/v2/stats/charts/market") + assert response = json_response(request, 200) + + assert response["chart_data"] == [] + assert response["available_supply"] == 0 + end + end + + describe "/stats/charts/transactions" do + test "get empty data", %{conn: conn} do + request = get(conn, "/api/v2/stats/charts/transactions") + assert response = json_response(request, 200) + + assert response["chart_data"] == [] + 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 new file mode 100644 index 000000000000..a0751c6bfee2 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -0,0 +1,228 @@ +defmodule BlockScoutWeb.API.V2.TokenControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.{Chain, Repo} + + alias Explorer.Chain.{Address, Token, TokenTransfer} + + alias Explorer.Chain.Address.CurrentTokenBalance + + describe "/tokens/{address_hash}" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get token", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}") + + assert response = json_response(request, 200) + + compare_item(token, response) + end + end + + describe "/tokens/{address_hash}/counters" do + test "get 404 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/counters") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get counters", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters") + + assert response = json_response(request, 200) + + assert response["transfers_count"] == "0" + assert response["token_holders_count"] == "0" + end + + test "get not zero counters", %{conn: conn} do + contract_token_address = insert(:contract_address) + token = insert(:token, contract_address: contract_token_address) + + transaction = + :transaction + |> insert(to_address: contract_token_address) + |> with_block() + + insert_list( + 3, + :token_transfer, + transaction: transaction, + token_contract_address: contract_token_address + ) + + second_page_token_balances = + 1..5 + |> Enum.map( + &insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: &1 + 1000 + ) + ) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters") + + assert response = json_response(request, 200) + + assert response["transfers_count"] == "3" + assert response["token_holders_count"] == "5" + end + end + + describe "/tokens/{address_hash}/transfers" do + test "get 200 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get empty list", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "check pagination", %{conn: conn} do + token = insert(:token) + + token_tranfers = + for _ <- 0..50 do + tx = insert(:transaction) |> with_block() + + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + token_contract_address: token.contract_address + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_tranfers) + end + end + + describe "/tokens/{address_hash}/holders" do + test "get 200 on non existing address", %{conn: conn} do + token = build(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/tokens/0x/holders") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get empty list", %{conn: conn} do + token = insert(:token) + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "check pagination", %{conn: conn} do + token = insert(:token) + + token_balances = + for i <- 0..50 do + insert( + :address_current_token_balance, + token_contract_address_hash: token.contract_address_hash, + value: i + 1000 + ) + end + + request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_balances) + end + end + + def compare_item(%Token{} = token, json) do + assert Address.checksum(token.contract_address.hash) == json["address"] + assert token.symbol == json["symbol"] + assert token.name == json["name"] + assert to_string(token.decimals) == json["decimals"] + assert token.type == json["type"] + assert token.holder_count == json["holders"] + assert to_string(token.total_supply) == json["total_supply"] + assert Map.has_key?(json, "exchange_rate") + end + + def compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["tx_hash"] + end + + def compare_item(%CurrentTokenBalance{} = ctb, json) do + assert Address.checksum(ctb.address_hash) == json["address"]["hash"] + assert ctb.token_id == json["token_id"] + assert to_string(ctb.value) == json["value"] + compare_item(Repo.preload(ctb, [{:token, :contract_address}]).token, json["token"]) + 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 +end 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 new file mode 100644 index 000000000000..036b79dfc946 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -0,0 +1,582 @@ +defmodule BlockScoutWeb.API.V2.TransactionControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.{Address, InternalTransaction, Log, TokenTransfer, Transaction} + + setup do + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + + :ok + end + + describe "/transactions" do + test "empty list", %{conn: conn} do + request = get(conn, "/api/v2/transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "non empty list", %{conn: conn} do + 1 + |> insert_list(:transaction) + |> with_block() + + request = get(conn, "/api/v2/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + end + + test "txs with next_page_params", %{conn: conn} do + txs = + 51 + |> insert_list(:transaction) + |> with_block() + + request = get(conn, "/api/v2/transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, txs) + end + + test "filter=pending", %{conn: conn} do + pending_txs = + 51 + |> insert_list(:transaction) + + _mined_txs = + 51 + |> insert_list(:transaction) + |> with_block() + + filter = %{"filter" => "pending"} + + request = get(conn, "/api/v2/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, pending_txs) + end + + test "filter=validated", %{conn: conn} do + _pending_txs = + 51 + |> insert_list(:transaction) + + mined_txs = + 51 + |> insert_list(:transaction) + |> with_block() + + filter = %{"filter" => "validated"} + + request = get(conn, "/api/v2/transactions", filter) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter)) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, mined_txs) + end + end + + describe "/transactions/{tx_hash}" do + test "return 404 on non existing tx", %{conn: conn} do + tx = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "return 422 on invalid tx hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return existing tx", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/" <> to_string(tx.hash)) + + assert response = json_response(request, 200) + compare_item(tx, response) + end + end + + describe "/transactions/{tx_hash}/internal-transactions" do + test "return empty list on non existing tx", %{conn: conn} do + tx = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return 422 on invalid tx hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/internal-transactions") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return empty list", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return relevant internal transaction", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + transaction: tx, + index: 0, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: 0 + ) + + internal_tx = + insert(:internal_transaction, + transaction: tx, + index: 1, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: 1 + ) + + tx_1 = + :transaction + |> insert() + |> with_block() + + 0..5 + |> Enum.map(fn index -> + insert(:internal_transaction, + transaction: tx_1, + index: index, + block_number: tx_1.block_number, + transaction_index: tx_1.index, + block_hash: tx_1.block_hash, + block_index: index + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(internal_tx, Enum.at(response["items"], 0)) + end + + test "return list with next_page_params", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + transaction: tx, + index: 0, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: 0 + ) + + internal_txs = + 51..1 + |> Enum.map(fn index -> + insert(:internal_transaction, + transaction: tx, + index: index, + block_number: tx.block_number, + transaction_index: tx.index, + block_hash: tx.block_hash, + block_index: index + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_txs) + end + end + + describe "/transactions/{tx_hash}/logs" do + test "return empty list on non existing tx", %{conn: conn} do + tx = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return 422 on invalid tx hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/logs") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return empty list", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return relevant log", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction: tx, + index: 1, + block: tx.block, + block_number: tx.block_number + ) + + tx_1 = + :transaction + |> insert() + |> with_block() + + 0..5 + |> Enum.map(fn index -> + insert(:log, + transaction: tx_1, + index: index, + block: tx_1.block, + block_number: tx_1.block_number + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + end + + test "return list with next_page_params", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + logs = + 50..0 + |> Enum.map(fn index -> + insert(:log, + transaction: tx, + index: index, + block: tx.block, + block_number: tx.block_number + ) + end) + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, logs) + end + end + + describe "/transactions/{tx_hash}/token-transfers" do + test "return empty list on non existing tx", %{conn: conn} do + tx = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return 422 on invalid tx hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/token-transfers") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return empty list", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "return relevant token transfer", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + token_transfer = insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number) + + tx_1 = + :transaction + |> insert() + |> with_block() + + insert_list(6, :token_transfer, transaction: tx_1, block: tx_1.block, block_number: tx_1.block_number) + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + end + + test "return list with next_page_params", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + token_transfers = + insert_list(51, :token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number) + |> Enum.reverse() + + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_transfers) + end + + test "check filters", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + erc_1155_token = insert(:token, type: "ERC-1155") + + erc_1155_tt = + for x <- 0..50 do + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + token_contract_address: erc_1155_token.contract_address, + token_ids: [x] + ) + end + |> Enum.reverse() + + erc_721_token = insert(:token, type: "ERC-721") + + erc_721_tt = + for x <- 0..50 do + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + |> Enum.reverse() + + erc_20_token = insert(:token, type: "ERC-20") + + erc_20_tt = + for _ <- 0..50 do + insert(:token_transfer, + transaction: tx, + block: tx.block, + block_number: tx.block_number, + token_contract_address: erc_20_token.contract_address + ) + end + |> Enum.reverse() + + # -- ERC-20 -- + filter = %{"type" => "ERC-20"} + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_20_tt) + # -- ------ -- + + # -- ERC-721 -- + filter = %{"type" => "ERC-721"} + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_721_tt) + # -- ------ -- + + # -- ERC-1155 -- + filter = %{"type" => "ERC-1155"} + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, erc_1155_tt) + # -- ------ -- + + # two filters simultaneously + filter = %{"type" => "ERC-1155,ERC-20"} + request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter) + assert response = json_response(request, 200) + + request_2nd_page = + get( + conn, + "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) + + 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(erc_1155_tt, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(erc_1155_tt, 1), Enum.at(response["items"], 49)) + + assert Enum.count(response_2nd_page["items"]) == 50 + assert response_2nd_page["next_page_params"] != nil + compare_item(Enum.at(erc_1155_tt, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(erc_20_tt, 2), Enum.at(response_2nd_page["items"], 49)) + + request_3rd_page = + get( + conn, + "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", + Map.merge(response_2nd_page["next_page_params"], filter) + ) + + assert response_3rd_page = json_response(request_3rd_page, 200) + assert Enum.count(response_3rd_page["items"]) == 2 + assert response_3rd_page["next_page_params"] == nil + compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1)) + end + end + + defp compare_item(%Transaction{} = transaction, json) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%InternalTransaction{} = internal_tx, json) do + assert internal_tx.block_number == json["block"] + assert to_string(internal_tx.gas) == json["gas_limit"] + assert internal_tx.index == json["index"] + assert to_string(internal_tx.transaction_hash) == json["transaction_hash"] + assert Address.checksum(internal_tx.from_address_hash) == json["from"]["hash"] + assert Address.checksum(internal_tx.to_address_hash) == json["to"]["hash"] + end + + defp compare_item(%Log{} = log, json) do + assert to_string(log.data) == json["data"] + assert log.index == json["index"] + assert Address.checksum(log.address_hash) == json["address"]["hash"] + end + + defp compare_item(%TokenTransfer{} = token_transfer, json) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == json["tx_hash"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, txs) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(txs, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(txs, 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(txs, 0), Enum.at(second_page_resp["items"], 0)) + end +end 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 a4adb8f8d1e6..0090ceaa9ee1 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,6 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do import BlockScoutWeb.WeiHelpers, only: [format_wei_value: 2] import EthereumJSONRPC, only: [integer_to_quantity: 1] alias Explorer.Chain.Wei - alias EthereumJSONRPC.Blocks describe "GET index/3" do test "loads existing transaction", %{conn: conn} do @@ -50,6 +49,35 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 1) end + test "returns state changes for the transaction with contract creation", %{conn: conn} do + block = insert(:block) + + contract_address = insert(:contract_address) + + transaction = + :transaction + |> insert(to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(insert(:block)) + + insert(:fetched_balance, + address_hash: transaction.from_address_hash, + value: 1_000_000, + block_number: block.number + ) + + insert(:fetched_balance, + address_hash: transaction.block.miner_hash, + value: 1_000_000, + block_number: block.number + ) + + conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"}) + {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode() + + assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 2) + end + test "returns fetched state changes for the transaction with token transfer", %{conn: conn} do block = insert(:block) address_a = insert(:address) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex index eda00ba3f582..5f01093dae0f 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex @@ -3,7 +3,7 @@ defmodule EthereumJSONRPC.HTTP do JSONRPC over HTTP """ - alias EthereumJSONRPC.Transport + alias EthereumJSONRPC.{DecodeError, Transport} require Logger @@ -125,7 +125,17 @@ defmodule EthereumJSONRPC.HTTP do {:error, {:bad_gateway, request_url}} _ -> - raise EthereumJSONRPC.DecodeError, named_arguments + named_arguments + |> DecodeError.exception() + |> DecodeError.message() + |> Logger.error() + + request_url = + named_arguments + |> Keyword.fetch!(:request) + |> Keyword.fetch!(:url) + + {:error, {:bad_response, request_url}} end end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex index bd491f25fd7f..b63e0971a6d3 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex @@ -109,7 +109,7 @@ defmodule EthereumJSONRPC.RequestCoordinator do defp trace_request(_, fun), do: fun.() - defp handle_transport_response({:error, {:bad_gateway, _}} = error) do + defp handle_transport_response({:error, {error_type, _}} = error) when error_type in [:bad_gateway, :bad_response] do RollingWindow.inc(table(), @error_key) inc_throttle_table() error diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 683ce9a48455..c2b74440aada 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -44,6 +44,20 @@ config :explorer, Explorer.Tracer, disabled?: false config :explorer, Explorer.Chain.Cache.MinMissingBlockNumber, enabled: false +# Configure API database +config :explorer, Explorer.Repo.Replica1, + 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, + 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), + implementation_data_fetching_timeout: :timer.seconds(20) + # Configure API database config :explorer, Explorer.Repo.Account, database: "explorer_test_account", diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index a01ba2b11e32..da426cf9fec7 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -20,6 +20,7 @@ defmodule Explorer.Application do MinMissingBlockNumber, NetVersion, Transactions, + TransactionsApiV2, Uncles } @@ -55,6 +56,7 @@ defmodule Explorer.Application do con_cache_child_spec(MarketHistoryCache.cache_name()), con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)), Transactions, + TransactionsApiV2, Accounts, Uncles, Supervisor.child_spec({Phoenix.PubSub, name: :chain_pubsub}, id: :chain_pubsub), diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 8208ef0058fe..cca29b609dc0 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -16,11 +16,11 @@ defmodule Explorer.Chain do order_by: 2, order_by: 3, preload: 2, + preload: 3, select: 2, select: 3, subquery: 1, union: 2, - update: 2, where: 2, where: 3 ] @@ -33,10 +33,9 @@ defmodule Explorer.Chain do alias Ecto.Adapters.SQL alias Ecto.{Changeset, Multi, Query} - alias EthereumJSONRPC.Contract alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction - alias Explorer.Counters.LastFetchedCounter + alias Explorer.Counters.{LastFetchedCounter, TokenHoldersCounter, TokenTransfersCounter} alias Explorer.Chain @@ -96,12 +95,15 @@ defmodule Explorer.Chain do alias Explorer.Counters.{ AddressesCounter, - AddressesWithBalanceCounter + AddressesWithBalanceCounter, + AddressTokenTransfersCounter, + AddressTransactionsCounter, + AddressTransactionsGasUsageCounter } alias Explorer.Market.MarketHistoryCache alias Explorer.{PagingOptions, Repo} - alias Explorer.SmartContract.{Helper, Reader} + alias Explorer.SmartContract.Helper alias Dataloader.Ecto, as: DataloaderEcto @@ -448,7 +450,7 @@ defmodule Explorer.Chain do options |> Keyword.get(:paging_options, @default_paging_options) - |> fetch_transactions(from_block, to_block) + |> fetch_transactions(from_block, to_block, true) end defp transactions_block_numbers_at_address(address_hash, options) do @@ -564,6 +566,20 @@ defmodule Explorer.Chain do |> Repo.all() end + @spec address_hash_to_token_transfers_new(Hash.Address.t() | String.t(), Keyword.t()) :: [TokenTransfer.t()] + def address_hash_to_token_transfers_new(address_hash, options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + direction = Keyword.get(options, :direction) + filters = Keyword.get(options, :token_type) + necessity_by_association = Keyword.get(options, :necessity_by_association) + + direction + |> TokenTransfer.token_transfers_by_address_hash(address_hash, filters) + |> join_associations(necessity_by_association) + |> TokenTransfer.handle_paging_options(paging_options) + |> Repo.all() + end + @doc """ address_hash_to_token_transfers_including_contract/2 function returns token transfers on address (to/from/contract). It is used by CSV export of token transfers button. @@ -2180,14 +2196,6 @@ defmodule Explorer.Chain do def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview - defp debug(value, key) do - require Logger - Logger.configure(truncate: :infinity) - Logger.info(key) - Logger.info(Kernel.inspect(value, limit: :infinity, printable_limit: :infinity)) - value - end - @doc """ Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for those `hashes`. @@ -3502,17 +3510,23 @@ defmodule Explorer.Chain do the `block_number` and `index` that are passed. """ - @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option], [String.t()], [ - :atom - ]) :: [ + @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option]) :: [ Transaction.t() ] - def recent_collated_transactions(old_ui?, options \\ [], method_id_filter \\ [], type_filter \\ []) + def recent_collated_transactions(old_ui?, options \\ []) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - fetch_recent_collated_transactions(old_ui?, paging_options, necessity_by_association, method_id_filter, type_filter) + method_id_filter = Keyword.get(options, :method) + type_filter = Keyword.get(options, :type) + + fetch_recent_collated_transactions( + old_ui?, + paging_options, + necessity_by_association, + method_id_filter, + type_filter + ) end # RAP - random access pagination @@ -3584,7 +3598,6 @@ defmodule Explorer.Chain do |> apply_filter_by_tx_type_to_transactions(type_filter) |> join_associations(necessity_by_association) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() - |> debug("result collated query") |> Repo.all() |> (&if(old_ui?, do: &1, @@ -3616,13 +3629,15 @@ defmodule Explorer.Chain do Results will be the transactions older than the `inserted_at` and `hash` that are passed. """ - @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false, [String.t()], [ - :atom - ]) :: [Transaction.t()] - def recent_pending_transactions(options \\ [], old_ui? \\ true, method_id_filter \\ [], type_filter \\ []) + @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false) :: [ + Transaction.t() + ] + def recent_pending_transactions(options \\ [], old_ui? \\ true) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = Keyword.get(options, :paging_options, @default_paging_options) + method_id_filter = Keyword.get(options, :method) + type_filter = Keyword.get(options, :type) Transaction |> page_pending_transaction(paging_options) @@ -3630,10 +3645,9 @@ defmodule Explorer.Chain do |> pending_transactions_query() |> apply_filter_by_method_id_to_transactions(method_id_filter) |> apply_filter_by_tx_type_to_transactions(type_filter) - |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash) + |> order_by([transaction], desc: transaction.inserted_at, asc: transaction.hash) |> join_associations(necessity_by_association) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() - |> debug("result pendging query") |> Repo.all() |> (&if(old_ui?, do: &1, @@ -3903,6 +3917,7 @@ defmodule Explorer.Chain do def transaction_to_token_transfers(transaction_hash, options \\ []) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = options |> Keyword.get(:paging_options, @default_paging_options) |> Map.put(:asc_order, true) + token_type = Keyword.get(options, :token_type) TokenTransfer |> join(:inner, [token_transfer], transaction in assoc(token_transfer, :transaction)) @@ -3911,6 +3926,9 @@ defmodule Explorer.Chain do transaction.hash == ^transaction_hash and token_transfer.block_hash == transaction.block_hash and token_transfer.block_number == transaction.block_number ) + |> join(:inner, [tt], token in assoc(tt, :token), as: :token) + |> preload([token: token], [{:token, token}]) + |> TokenTransfer.filter_by_type(token_type) |> TokenTransfer.page_token_transfer(paging_options) |> limit(^paging_options.page_size) |> order_by([token_transfer], asc: token_transfer.log_index) @@ -4424,6 +4442,30 @@ defmodule Explorer.Chain do |> repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name]) end + def get_verified_twin_contract(%Explorer.Chain.Address{} = target_address) 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 + |> Repo.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 @@ -4437,24 +4479,8 @@ defmodule Explorer.Chain do def get_address_verified_twin_contract(%Explorer.Chain.Hash{} = address_hash) do with target_address <- Repo.get(Address, address_hash), - false <- is_nil(target_address), - %{contract_code: %Chain.Data{bytes: contract_code_bytes}} <- target_address do - 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 = - verified_contract_twin_query - |> Repo.one(timeout: 10_000) + false <- is_nil(target_address) do + verified_contract_twin = get_verified_twin_contract(target_address) verified_contract_twin_additional_sources = get_contract_additional_sources(verified_contract_twin) @@ -4547,13 +4573,28 @@ defmodule Explorer.Chain do Chain.get_address_verified_twin_contract(address_hash).verified_contract if address_verified_twin_contract do - Map.put(address_verified_twin_contract, :address_hash, address_hash) + address_verified_twin_contract + |> Map.put(:address_hash, address_hash) + |> Map.put(:implementation_address_hash, current_smart_contract.implementation_address_hash) + |> Map.put(:implementation_name, current_smart_contract.implementation_name) + |> Map.put(:implementation_fetched_at, current_smart_contract.implementation_fetched_at) else current_smart_contract end end end + @spec address_hash_to_smart_contract_without_twin(Hash.Address.t()) :: SmartContract.t() | nil + def address_hash_to_smart_contract_without_twin(address_hash) do + query = + from( + smart_contract in SmartContract, + where: smart_contract.address_hash == ^address_hash + ) + + Repo.one(query) + end + def smart_contract_fully_verified?(address_hash_str) when is_binary(address_hash_str) do case string_to_address_hash(address_hash_str) do {:ok, address_hash} -> @@ -4604,13 +4645,28 @@ defmodule Explorer.Chain do if Repo.one(query), do: true, else: false end - defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil) do + defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil, is_address? \\ false) do Transaction - |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) + |> order_for_transactions(is_address?) |> 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], desc: transaction.block_number, asc: transaction.index) @@ -4821,7 +4877,10 @@ defmodule Explorer.Chain do where( query, [transaction], - transaction.inserted_at < ^inserted_at or (transaction.inserted_at == ^inserted_at and transaction.hash < ^hash) + (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 @@ -4854,6 +4913,20 @@ defmodule Explorer.Chain do defp page_search_results(query, %PagingOptions{key: nil}), do: query + defp page_search_results(query, %PagingOptions{ + key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type} + }) + when holder_count in [nil, ""] do + where( + query, + [item], + (item.name > ^name and item.type == ^item_type) or + (item.name == ^name and item.inserted_at < ^inserted_at and + item.type == ^item_type) or + item.type != ^item_type + ) + end + # credo:disable-for-next-line defp page_search_results(query, %PagingOptions{ key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type} @@ -6413,12 +6486,13 @@ defmodule Explorer.Chain do end end - def combine_proxy_implementation_abi(proxy_address_hash, abi) when not is_nil(abi) do - implementation_abi = get_implementation_abi_from_proxy(proxy_address_hash, abi) + def combine_proxy_implementation_abi(%SmartContract{abi: abi} = smart_contract) when not is_nil(abi) do + implementation_abi = get_implementation_abi_from_proxy(smart_contract) + if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi end - def combine_proxy_implementation_abi(_, abi) when is_nil(abi) do + def combine_proxy_implementation_abi(_) do [] end @@ -6450,10 +6524,7 @@ defmodule Explorer.Chain do def gnosis_safe_contract?(abi) when is_nil(abi), do: false - defp master_copy_pattern?(method) do - method - |> contructor_accepts_named_input("_masterCopy") - end + defp singleton_pattern?(method) do method @@ -6474,222 +6545,32 @@ defmodule Explorer.Chain do end) end - defp get_input_by_name(inputs, name) do - inputs - |> Enum.find(fn input -> - Map.get(input, "name") == name - end) - end - - @spec get_implementation_address_hash(Hash.Address.t(), list()) :: {String.t() | nil, String.t() | nil} - def get_implementation_address_hash(proxy_address_hash, abi) - 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) - - master_copy_method_abi = - abi - |> Enum.find(fn method -> - master_copy_pattern?(method) - end) - - implementation_address = - cond do - implementation_method_abi -> - get_implementation_address_hash_basic(proxy_address_hash, abi) - - master_copy_method_abi -> - get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) - - true -> - get_implementation_address_hash_eip_1967(proxy_address_hash) - end - - save_implementation_name(implementation_address, proxy_address_hash) - end - - def get_implementation_address_hash(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do - {nil, nil} - end - - defp get_implementation_address_hash_eip_1967(proxy_address_hash) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - # https://eips.ethereum.org/EIPS/eip-1967 - storage_slot_logic_contract_address = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" - - {_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 empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] -> - fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) - - {:ok, implementation_logic_address} -> - {:ok, implementation_logic_address} - - {:error, _} -> - {:ok, "0x"} - end - - abi_decode_address_output(implementation_address) - 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" => [] - } - ] - - 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 empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] -> - 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_method_abi) do - <> -> - {:ok, implementation_address} + 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) _ -> - {:ok, beacon_contract_address} + false end - - {:error, _} -> - {:ok, "0x"} - 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" - - 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 empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] -> - {:ok, "0x"} - - {:ok, logic_contract_address} -> - {:ok, logic_contract_address} - - {:error, _} -> - {:ok, "0x"} - end - end - - defp get_implementation_address_hash_basic(proxy_address_hash, abi) do - # 5c60da1b = keccak256(implementation()) - implementation_address = - case Reader.query_contract( - proxy_address_hash, - abi, - %{ - "5c60da1b" => [] - }, - false - ) do - %{"5c60da1b" => {:ok, [result]}} -> result - _ -> nil - end - - address_to_hex(implementation_address) - 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) - - master_copy_storage_pointer = "0x0" - - {:ok, implementation_address} = - Contract.eth_get_storage_at_request( - proxy_address_hash, - master_copy_storage_pointer, - nil, - json_rpc_named_arguments - ) - - abi_decode_address_output(implementation_address) - end - - defp save_implementation_name(empty_address_hash_string, _) - when empty_address_hash_string in [ - "0x", - "0x0", - "0x0000000000000000000000000000000000000000000000000000000000000000", - @burn_address_hash_str - ], - do: {empty_address_hash_string, nil} - - defp save_implementation_name(implementation_address_hash_string, proxy_address_hash) - when is_binary(implementation_address_hash_string) do - with {:ok, address_hash} <- string_to_address_hash(implementation_address_hash_string), - %SmartContract{name: name} <- address_hash_to_smart_contract(address_hash) do - SmartContract - |> where([sc], sc.address_hash == ^proxy_address_hash) - |> update(set: [implementation_name: ^name]) - |> Repo.update_all([]) - - {implementation_address_hash_string, name} - else - _ -> - {implementation_address_hash_string, nil} - end + end) end - defp save_implementation_name(other, _), do: {other, nil} - - defp abi_decode_address_output(nil), do: nil - - defp abi_decode_address_output("0x"), do: @burn_address_hash_str - - defp 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 + defp get_input_by_name(inputs, name) do + inputs + |> Enum.find(fn input -> + Map.get(input, "name") == name + end) end - defp abi_decode_address_output(_), do: nil - - 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 - 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) when not is_nil(implementation_address_hash_string) do @@ -6715,15 +6596,13 @@ defmodule Explorer.Chain do [] end - def get_implementation_abi_from_proxy(proxy_address_hash, abi) + def get_implementation_abi_from_proxy(%SmartContract{address_hash: proxy_address_hash, abi: abi} = smart_contract) when not is_nil(proxy_address_hash) and not is_nil(abi) do - {implementation_address_hash_string, _name} = get_implementation_address_hash(proxy_address_hash, abi) + {implementation_address_hash_string, _name} = SmartContract.get_implementation_address_hash(smart_contract) get_implementation_abi(implementation_address_hash_string) end - def get_implementation_abi_from_proxy(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do - [] - 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} = @@ -7076,10 +6955,16 @@ defmodule Explorer.Chain do recent_pending_transactions(options, false, method_id_filter, type_filter_options) end - def recent_transactions(options, _, method_id_filter, type_filter_options) do - recent_collated_transactions(false, options, method_id_filter, type_filter_options) + def recent_transactions(options, [:pending | _]) do + recent_pending_transactions(options, false) + end + + def recent_transactions(options, _) do + recent_collated_transactions(false, options) end + def apply_filter_by_method_id_to_transactions(query, nil), do: query + def apply_filter_by_method_id_to_transactions(query, filter) when is_list(filter) do method_ids = Enum.flat_map(filter, &map_name_or_method_id_to_method_id/1) @@ -7290,4 +7175,83 @@ defmodule Explorer.Chain do def count_new_contracts_from_cache do NewContractsCounter.fetch() end + + def address_counters(address) do + validation_count_task = + Task.async(fn -> + address_to_validation_count(address.hash) + end) + + Task.start_link(fn -> + transaction_count(address) + end) + + Task.start_link(fn -> + token_transfers_count(address) + end) + + Task.start_link(fn -> + gas_usage_count(address) + end) + + [ + validation_count_task + ] + |> Task.yield_many(:infinity) + |> Enum.map(fn {_task, res} -> + case res do + {:ok, result} -> + result + + {:exit, reason} -> + raise "Query fetching address counters terminated: #{inspect(reason)}" + + nil -> + raise "Query fetching address counters timed out." + end + end) + |> List.to_tuple() + end + + def transaction_count(address) do + AddressTransactionsCounter.fetch(address) + end + + def token_transfers_count(address) do + AddressTokenTransfersCounter.fetch(address) + end + + def gas_usage_count(address) do + AddressTransactionsGasUsageCounter.fetch(address) + end + + def fetch_token_counters(address_hash, timeout) do + total_token_transfers_task = + Task.async(fn -> + TokenTransfersCounter.fetch(address_hash) + end) + + total_token_holders_task = + Task.async(fn -> + TokenHoldersCounter.fetch(address_hash) + end) + + [total_token_transfers_task, total_token_holders_task] + |> Task.yield_many(timeout) + |> Enum.map(fn {_task, res} -> + case res do + {:ok, result} -> + result + + {:exit, reason} -> + Logger.warn("Query fetching token counters terminated: #{inspect(reason)}") + 0 + + nil -> + Logger.warn("Query fetching token counters timed out.") + 0 + end + end) + |> List.to_tuple() + end end 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 4cf3fbd813d0..93174cb70c5f 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -23,7 +23,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance 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) * `token_type` - The type of the token """ @type t :: %__MODULE__{ diff --git a/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex b/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex new file mode 100644 index 000000000000..0a5cf715c1bc --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex @@ -0,0 +1,27 @@ +defmodule Explorer.Chain.Cache.TransactionsApiV2 do + @moduledoc """ + Caches the latest imported transactions + """ + + alias Explorer.Chain.Transaction + + use Explorer.Chain.OrderedCache, + name: :transactions_api_v2, + max_size: 51, + preloads: [ + :block, + created_contract_address: :names, + from_address: :names, + to_address: :names + ], + ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval], + global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl] + + @type element :: Transaction.t() + + @type id :: {non_neg_integer(), non_neg_integer()} + + def element_to_id(%Transaction{block_number: block_number, index: index}) do + {block_number, index} + end +end 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 c33d7cf24d37..e0f0caa99801 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 @@ -220,51 +220,15 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md) - %{ - changes_list_no_token_id: changes_list_no_token_id, - changes_list_with_token_id: changes_list_with_token_id - } = + ordered_changes_list = changes_list - |> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc -> - updated_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) - end - - if updated_change.token_id do - changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id] - - %{ - changes_list_no_token_id: acc.changes_list_no_token_id, - changes_list_with_token_id: changes_list_with_token_id - } + |> Enum.map(fn change -> + if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do + change else - changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id] - - %{ - changes_list_no_token_id: changes_list_no_token_id, - changes_list_with_token_id: acc.changes_list_with_token_id - } + Map.put(change, :token_id, nil) end end) - - ordered_changes_list_no_token_id = - changes_list_no_token_id - |> Enum.group_by(fn %{ - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash - } -> - {address_hash, token_contract_address_hash} - end) - |> Enum.map(fn {_, grouped_address_token_balances} -> - Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end) - end) - |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash}) - - ordered_changes_list_with_token_id = - changes_list_with_token_id |> Enum.group_by(fn %{ address_hash: address_hash, token_contract_address_hash: token_contract_address_hash, @@ -273,33 +237,18 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do {address_hash, token_contract_address_hash, token_id} end) |> Enum.map(fn {_, grouped_address_token_balances} -> - Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end) + Enum.max_by(grouped_address_token_balances, fn balance -> + {Map.get(balance, :block_number), Map.get(balance, :value_fetched_at)} + end) end) |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash}) - {:ok, inserted_changes_list_no_token_id} = - if Enum.count(ordered_changes_list_no_token_id) > 0 do - Import.insert_changes_list( - repo, - ordered_changes_list_no_token_id, - conflict_target: {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash) WHERE token_id IS NULL>}, - on_conflict: on_conflict, - for: CurrentTokenBalance, - returning: true, - timeout: timeout, - timestamps: timestamps - ) - else - {:ok, []} - end - - {:ok, inserted_changes_list_with_token_id} = - if Enum.count(ordered_changes_list_with_token_id) > 0 do + {:ok, inserted_changes_list} = + if Enum.count(ordered_changes_list) > 0 do Import.insert_changes_list( repo, - ordered_changes_list_with_token_id, - conflict_target: - {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, token_id) WHERE token_id IS NOT NULL>}, + ordered_changes_list, + conflict_target: {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, COALESCE(token_id, -1))>}, on_conflict: on_conflict, for: CurrentTokenBalance, returning: true, @@ -310,7 +259,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do {:ok, []} end - inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id + inserted_changes_list end defp default_on_conflict do @@ -329,9 +278,10 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do ], where: fragment("? < EXCLUDED.block_number", current_token_balance.block_number) or - (fragment("EXCLUDED.value IS NOT NULL") and - is_nil(current_token_balance.value_fetched_at) and - fragment("? = EXCLUDED.block_number", current_token_balance.block_number)) + (fragment("? = EXCLUDED.block_number", current_token_balance.block_number) and + fragment("EXCLUDED.value IS NOT NULL") and + (is_nil(current_token_balance.value_fetched_at) or + fragment("? < EXCLUDED.value_fetched_at", current_token_balance.value_fetched_at))) ) end 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 d80f561bbe7a..e991427de4d8 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 @@ -60,58 +60,15 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce TokenBalance ShareLocks order (see docs: sharelocks.md) - %{ - changes_list_no_token_id: changes_list_no_token_id, - changes_list_with_token_id: changes_list_with_token_id - } = + ordered_changes_list = changes_list - |> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc -> - updated_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) - end - - if updated_change.token_id do - changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id] - - %{ - changes_list_no_token_id: acc.changes_list_no_token_id, - changes_list_with_token_id: changes_list_with_token_id - } + |> Enum.map(fn change -> + if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do + change else - changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id] - - %{ - changes_list_no_token_id: changes_list_no_token_id, - changes_list_with_token_id: acc.changes_list_with_token_id - } + Map.put(change, :token_id, nil) end end) - - ordered_changes_list_no_token_id = - changes_list_no_token_id - |> Enum.group_by(fn %{ - address_hash: address_hash, - token_contract_address_hash: token_contract_address_hash, - block_number: block_number - } -> - {token_contract_address_hash, address_hash, block_number} - end) - |> Enum.map(fn {_, grouped_address_token_balances} -> - dedup = Enum.dedup(grouped_address_token_balances) - - if Enum.count(dedup) > 1 do - Enum.max_by(dedup, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end) - else - Enum.at(dedup, 0) - end - end) - |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash, &1.block_number}) - - ordered_changes_list_with_token_id = - changes_list_with_token_id |> Enum.group_by(fn %{ address_hash: address_hash, token_contract_address_hash: token_contract_address_hash, @@ -122,20 +79,20 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do end) |> Enum.map(fn {_, grouped_address_token_balances} -> if Enum.count(grouped_address_token_balances) > 1 do - Enum.max_by(grouped_address_token_balances, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end) + Enum.max_by(grouped_address_token_balances, fn balance -> Map.get(balance, :value_fetched_at) end) else Enum.at(grouped_address_token_balances, 0) end end) |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash, &1.block_number}) - {:ok, inserted_changes_list_no_token_id} = - if Enum.count(ordered_changes_list_no_token_id) > 0 do + {:ok, inserted_changes_list} = + if Enum.count(ordered_changes_list) > 0 do Import.insert_changes_list( repo, - ordered_changes_list_no_token_id, + ordered_changes_list, conflict_target: - {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, block_number) WHERE token_id IS NULL>}, + {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, COALESCE(token_id, -1), block_number)>}, on_conflict: on_conflict, for: TokenBalance, returning: true, @@ -146,26 +103,6 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do {:ok, []} end - {:ok, inserted_changes_list_with_token_id} = - if Enum.count(ordered_changes_list_with_token_id) > 0 do - Import.insert_changes_list( - repo, - ordered_changes_list_with_token_id, - conflict_target: - {:unsafe_fragment, - ~s<(address_hash, token_contract_address_hash, token_id, block_number) WHERE token_id IS NOT NULL>}, - on_conflict: on_conflict, - for: TokenBalance, - returning: true, - timeout: timeout, - timestamps: timestamps - ) - else - {:ok, []} - end - - inserted_changes_list = inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id - {:ok, inserted_changes_list} end @@ -174,7 +111,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do token_balance in TokenBalance, update: [ set: [ - value: fragment("EXCLUDED.value"), + value: fragment("COALESCE(EXCLUDED.value, ?)", token_balance.value), value_fetched_at: fragment("EXCLUDED.value_fetched_at"), token_type: fragment("EXCLUDED.token_type"), inserted_at: fragment("LEAST(EXCLUDED.inserted_at, ?)", token_balance.inserted_at), @@ -182,9 +119,8 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do ] ], where: - fragment("EXCLUDED.value IS NOT NULL") and - (is_nil(token_balance.value_fetched_at) or - fragment("? < EXCLUDED.value_fetched_at", token_balance.value_fetched_at)) + is_nil(token_balance.value_fetched_at) or fragment("EXCLUDED.value_fetched_at IS NULL") or + fragment("? < EXCLUDED.value_fetched_at", token_balance.value_fetched_at) ) end end diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 013549865be0..d8529a7a260a 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -131,8 +131,8 @@ defmodule Explorer.Chain.Log do ] case Chain.find_contract_address(log.address_hash, address_options, true) do - {:ok, %{smart_contract: %{abi: abi}}} -> - full_abi = Chain.combine_proxy_implementation_abi(log.address_hash, abi) + {:ok, %{smart_contract: smart_contract}} -> + full_abi = Chain.combine_proxy_implementation_abi(smart_contract) with {:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction), identifier <- Base.encode16(selector.method_id, case: :lower), diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 5fe6f3668383..56724590e797 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -13,9 +13,15 @@ defmodule Explorer.Chain.SmartContract do use Explorer.Schema alias Ecto.Changeset + alias EthereumJSONRPC.Contract + 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 Timex.Duration + + @burn_address_hash_str "0x0000000000000000000000000000000000000000" @typedoc """ The name of a parameter to a function or event. @@ -197,11 +203,15 @@ defmodule Explorer.Chain.SmartContract do * `partially_verified` - whether contract verified using partial matched source code or not. * `is_vyper_contract` - boolean flag, determines if contract is Vyper or not * `file_path` - show the filename or path to the file of the contract source file - * `is_changed_bytecode` - boolean flag, determines if contract's bytecode was modified + * `is_changed_bytecode` - boolean flag, determines if contract's bytecode was modified * `bytecode_checked_at` - timestamp of the last check of contract's bytecode matching (DB and BlockChain) * `contract_code_md5` - md5(`t:Explorer.Chain.Address.t/0` `contract_code`) * `implementation_name` - name of the proxy implementation + * `compiler_settings` - raw compilation parameters + * `implementation_fetched_at` - timestamp of the last fetching contract's implementation info + * `implementation_address_hash` - address hash of the proxy's implementation if any * `autodetect_constructor_args` - field was added for storing user's choice + * `is_yul` - field was added for storing user's choice """ @type t :: %Explorer.Chain.SmartContract{ @@ -221,7 +231,11 @@ defmodule Explorer.Chain.SmartContract do bytecode_checked_at: DateTime.t(), contract_code_md5: String.t(), implementation_name: String.t() | nil, - autodetect_constructor_args: boolean | 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 } schema "smart_contracts" do @@ -242,7 +256,11 @@ defmodule Explorer.Chain.SmartContract do field(:bytecode_checked_at, :utc_datetime_usec, default: DateTime.add(DateTime.utc_now(), -86400, :second)) field(:contract_code_md5, :string) field(:implementation_name, :string) + field(:compiler_settings, :map) + field(:implementation_fetched_at, :utc_datetime_usec, default: nil) + field(:implementation_address_hash, Hash.Address, default: nil) field(:autodetect_constructor_args, :boolean, virtual: true) + field(:is_yul, :boolean, virtual: true) has_many( :decompiled_smart_contracts, @@ -284,14 +302,16 @@ defmodule Explorer.Chain.SmartContract do :is_changed_bytecode, :bytecode_checked_at, :contract_code_md5, - :implementation_name + :implementation_name, + :compiler_settings, + :implementation_address_hash, + :implementation_fetched_at ]) |> validate_required([ :name, :compiler_version, :optimization, :contract_source_code, - :abi, :address_hash, :contract_code_md5 ]) @@ -431,6 +451,7 @@ defmodule Explorer.Chain.SmartContract do %__MODULE__{} |> changeset(Map.from_struct(twin_contract)) |> Changeset.put_change(:autodetect_constructor_args, true) + |> Changeset.put_change(:is_yul, false) |> Changeset.force_change(:address_hash, Changeset.get_field(changeset, :address_hash)) end @@ -443,6 +464,7 @@ defmodule Explorer.Chain.SmartContract do |> Changeset.put_change(:compiler_version, "latest") |> Changeset.put_change(:contract_source_code, "") |> Changeset.put_change(:autodetect_constructor_args, true) + |> Changeset.put_change(:is_yul, false) end def merge_twin_vyper_contract_with_changeset( @@ -485,4 +507,370 @@ defmodule Explorer.Chain.SmartContract do end defp to_address_hash(address_hash), do: address_hash + + def proxy_contract?(%__MODULE__{abi: abi} = smart_contract) 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() + |> Tuple.to_list() + |> List.first() + ), + do: true, + else: false + end + + def proxy_contract?(_), do: false + + def get_implementation_address_hash(%__MODULE__{abi: nil}), do: false + + def get_implementation_address_hash( + %__MODULE__{ + address_hash: address_hash, + implementation_fetched_at: implementation_fetched_at + } = smart_contract + ) do + updated_smart_contract = + if Application.get_env(:explorer, :enable_caching_implementation_data_of_proxy) && + check_implementation_refetch_neccessity(implementation_fetched_at) do + Chain.address_hash_to_smart_contract(address_hash) + else + smart_contract + end + + get_implementation_address_hash({:updated, updated_smart_contract}) + end + + def get_implementation_address_hash( + {:updated, + %__MODULE__{ + address_hash: address_hash, + abi: abi, + implementation_address_hash: implementation_address_hash_from_db, + implementation_name: implementation_name_from_db, + implementation_fetched_at: implementation_fetched_at + }} + ) do + if check_implementation_refetch_neccessity(implementation_fetched_at) do + get_implementation_address_hash_task = Task.async(fn -> get_implementation_address_hash(address_hash, abi) end) + + timeout = Application.get_env(:explorer, :implementation_data_fetching_timeout) + + case Task.yield(get_implementation_address_hash_task, timeout) || + Task.ignore(get_implementation_address_hash_task) do + {:ok, {:empty, :empty}} -> + {nil, nil} + + {:ok, {address_hash, _name} = result} when not is_nil(address_hash) -> + result + + _ -> + {db_implementation_data_converter(implementation_address_hash_from_db), + db_implementation_data_converter(implementation_name_from_db)} + end + else + {db_implementation_data_converter(implementation_address_hash_from_db), + db_implementation_data_converter(implementation_name_from_db)} + end + end + + 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) + + defp check_implementation_refetch_neccessity(nil), do: true + + defp check_implementation_refetch_neccessity(timestamp) do + if Application.get_env(:explorer, :enable_caching_implementation_data_of_proxy) do + now = DateTime.utc_now() + + average_block_time = + 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 + + duration -> + duration + |> Duration.to_milliseconds() + end + else + 0 + end + + fresh_time_distance = + case average_block_time do + 0 -> + Application.get_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy) + + time -> + round(time) + end + + timestamp + |> DateTime.add(fresh_time_distance, :millisecond) + |> DateTime.compare(now) != :gt + else + true + end + end + + @spec get_implementation_address_hash(Hash.Address.t(), list()) :: {String.t() | nil, String.t() | nil} + defp get_implementation_address_hash(proxy_address_hash, abi) + 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) + + master_copy_method_abi = + abi + |> Enum.find(fn method -> + Chain.master_copy_pattern?(method) + end) + + implementation_address = + cond do + implementation_method_abi -> + get_implementation_address_hash_basic(proxy_address_hash, abi) + + master_copy_method_abi -> + get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) + + true -> + get_implementation_address_hash_eip_1967(proxy_address_hash) + end + + save_implementation_data(implementation_address, proxy_address_hash) + end + + defp get_implementation_address_hash(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do + {nil, nil} + end + + defp get_implementation_address_hash_eip_1967(proxy_address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + # https://eips.ethereum.org/EIPS/eip-1967 + storage_slot_logic_contract_address = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + + {_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 empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000", nil] -> + fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) + + {:ok, implementation_logic_address} -> + {:ok, implementation_logic_address} + + _ -> + {:ok, nil} + end + + abi_decode_address_output(implementation_address) + 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" => [] + } + ] + + 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 empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000", nil] -> + 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_method_abi) do + <> -> + {:ok, implementation_address} + + _ -> + {:ok, beacon_contract_address} + end + + _ -> + {:ok, nil} + 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" + + 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 empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] -> + {:ok, "0x"} + + {:ok, logic_contract_address} -> + {:ok, logic_contract_address} + + _ -> + {:ok, nil} + end + end + + defp get_implementation_address_hash_basic(proxy_address_hash, abi) do + # 5c60da1b = keccak256(implementation()) + implementation_address = + case Reader.query_contract( + proxy_address_hash, + abi, + %{ + "5c60da1b" => [] + }, + false + ) do + %{"5c60da1b" => {:ok, [result]}} -> result + _ -> nil + end + + address_to_hex(implementation_address) + 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) + + 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 empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] -> + {:ok, "0x"} + + {:ok, logic_contract_address} -> + {:ok, logic_contract_address} + + _ -> + {:ok, nil} + end + + abi_decode_address_output(implementation_address) + end + + defp save_implementation_data(nil, _), do: {nil, nil} + + defp save_implementation_data(empty_address_hash_string, proxy_address_hash) + when empty_address_hash_string in [ + "0x", + "0x0", + "0x0000000000000000000000000000000000000000000000000000000000000000", + @burn_address_hash_str + ] do + proxy_address_hash + |> Chain.address_hash_to_smart_contract_without_twin() + |> changeset(%{ + implementation_name: nil, + implementation_address_hash: nil, + implementation_fetched_at: DateTime.utc_now() + }) + |> Repo.update() + + {:empty, :empty} + end + + defp save_implementation_data(implementation_address_hash_string, proxy_address_hash) + 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), + false <- is_nil(proxy_contract), + %{implementation: %__MODULE__{name: name}, proxy: proxy_contract} <- %{ + implementation: Chain.address_hash_to_smart_contract(address_hash), + 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() + + {implementation_address_hash_string, nil} + + _ -> + {implementation_address_hash_string, nil} + 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 + end + end + + defp abi_decode_address_output(nil), do: nil + + defp abi_decode_address_output("0x"), do: @burn_address_hash_str + + defp 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 + + defp abi_decode_address_output(_), do: nil end diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index e203f7ada899..dc5cf7eb9ca1 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -25,7 +25,7 @@ defmodule Explorer.Chain.TokenTransfer do use Explorer.Schema import Ecto.Changeset - import Ecto.Query, only: [from: 2, limit: 2, where: 3] + import Ecto.Query, only: [from: 2, limit: 2, where: 3, join: 5, order_by: 3, preload: 3] alias Explorer.Chain.{Address, Block, Hash, TokenTransfer, Transaction} alias Explorer.Chain.Token.Instance @@ -247,6 +247,16 @@ defmodule Explorer.Chain.TokenTransfer do ) end + 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_token_transfer(paging_options) + |> limit(^paging_options.page_size) + end + @doc """ Fetches the transaction hashes from token transfers according to the address hash. @@ -310,4 +320,36 @@ defmodule Explorer.Chain.TokenTransfer do tt.block_number < ^block_number ) 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) + end + + def filter_by_direction(query, :to, address_hash) do + query + |> where([tt], tt.to_address_hash == ^address_hash) + end + + def filter_by_direction(query, :from, address_hash) do + query + |> 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) + end + + def filter_by_type(query, _), do: query end diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index eff752277a86..c170bb9475e1 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -23,6 +23,7 @@ defmodule Explorer.Chain.Transaction do Hash, InternalTransaction, Log, + SmartContract, Token, TokenTransfer, Transaction, @@ -521,7 +522,7 @@ defmodule Explorer.Chain.Transaction do candidates_query |> Repo.all() |> Enum.flat_map(fn candidate -> - case do_decoded_input_data(data, [candidate.abi], nil, hash) do + case do_decoded_input_data(data, %SmartContract{abi: [candidate.abi], address_hash: nil}, hash) do {:ok, _, _, _} = decoded -> [decoded] _ -> [] end @@ -536,11 +537,11 @@ defmodule Explorer.Chain.Transaction do def decoded_input_data(%__MODULE__{ input: %{bytes: data}, - to_address: %{smart_contract: %{abi: abi, address_hash: address_hash}}, + to_address: %{smart_contract: smart_contract}, hash: hash }) do - case do_decoded_input_data(data, abi, address_hash, hash) do - # In some cases transactions use methods of some unpredictable contracts, so we can try to look up for method in a whole DB + case do_decoded_input_data(data, smart_contract, hash) do + # In some cases transactions use methods of some unpredictadle contracts, so we can try to look up for method in a whole DB {:error, :could_not_decode} -> case decoded_input_data(%__MODULE__{ to_address: %{smart_contract: nil}, @@ -562,8 +563,8 @@ defmodule Explorer.Chain.Transaction do end end - defp do_decoded_input_data(data, abi, address_hash, hash) do - full_abi = Chain.combine_proxy_implementation_abi(address_hash, abi) + defp do_decoded_input_data(data, smart_contract, hash) do + full_abi = Chain.combine_proxy_implementation_abi(smart_contract) with {:ok, {selector, values}} <- find_and_decode(full_abi, data, hash), {:ok, mapping} <- selector_mapping(selector, values, hash), diff --git a/apps/explorer/lib/explorer/etherscan/contracts.ex b/apps/explorer/lib/explorer/etherscan/contracts.ex index 089ef7538f83..253d4bde9118 100644 --- a/apps/explorer/lib/explorer/etherscan/contracts.ex +++ b/apps/explorer/lib/explorer/etherscan/contracts.ex @@ -74,13 +74,13 @@ 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 Chain.proxy_contract?(address.hash, smart_contract.abi) do + if SmartContract.proxy_contract?(smart_contract) do smart_contract |> Map.put(:is_proxy, true) |> Map.put( :implementation_address_hash_string, - address.hash - |> Chain.get_implementation_address_hash(smart_contract.abi) + smart_contract + |> SmartContract.get_implementation_address_hash() |> Tuple.to_list() |> List.first() ) diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index 3ef71da72e4f..68cff935e03e 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -565,15 +565,12 @@ defmodule Explorer.SmartContract.Reader do end defp get_abi(contract_address_hash, type) do - abi = - contract_address_hash - |> Chain.address_hash_to_smart_contract() - |> Map.get(:abi) + contract = Chain.address_hash_to_smart_contract(contract_address_hash) if type == :proxy do - Chain.get_implementation_abi_from_proxy(contract_address_hash, abi) + Chain.get_implementation_abi_from_proxy(contract) else - abi + contract.abi end end diff --git a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex index e54fcddd7b62..34a2eab02dcf 100644 --- a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex @@ -2,6 +2,7 @@ defmodule Explorer.SmartContract.RustVerifierInterface do @moduledoc """ Adapter for contracts verification with https://github.com/blockscout/blockscout-rs/blob/main/smart-contract-verifier """ + alias Explorer.Utility.RustService alias HTTPoison.Response require Logger @@ -47,7 +48,7 @@ defmodule Explorer.SmartContract.RustVerifierInterface do def http_post_request(url, body) 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!(normalize_creation_bytecode(body)), headers, recv_timeout: @post_timeout) do {:ok, %Response{body: body, status_code: 200}} -> proccess_verifier_response(body) @@ -124,6 +125,10 @@ defmodule Explorer.SmartContract.RustVerifierInterface do def proccess_verifier_response(other), do: {:error, other} + def normalize_creation_bytecode(%{"creation_bytecode" => ""} = map), do: Map.replace(map, "creation_bytecode", nil) + + def normalize_creation_bytecode(map), do: map + def multiple_files_verification_url, do: "#{base_api_url()}" <> "/solidity/verify/multiple-files" def vyper_multiple_files_verification_url, do: "#{base_api_url()}" <> "/vyper/verify/multiple-files" @@ -137,14 +142,7 @@ defmodule Explorer.SmartContract.RustVerifierInterface do def base_api_url, do: "#{base_url()}" <> "/api/v1" def base_url 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 + RustService.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 c7ea1dd25e4e..0cb5cb46407b 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex @@ -52,8 +52,9 @@ defmodule Explorer.SmartContract.Solidity.Publisher do |> Map.put("contract_source_code", contract_source_code) |> Map.put("external_libraries", contract_libraries) |> Map.put("name", contract_name) + |> cast_compiler_settings(false) - publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string)) + publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string || "null")) {:ok, %{abi: abi, constructor_arguments: constructor_arguments}} -> params_with_constructor_arguments = @@ -90,7 +91,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do "optimization_runs" => _, "sources" => _ } = result_params} -> - proccess_rust_verifier_response(result_params, address_hash) + proccess_rust_verifier_response(result_params, address_hash, true) {:ok, %{abi: abi, constructor_arguments: constructor_arguments}, additional_params} -> params_with_constructor_arguments = @@ -155,7 +156,8 @@ defmodule Explorer.SmartContract.Solidity.Publisher do "optimization_runs" => _, "sources" => sources } = result_params, - address_hash + address_hash, + is_standard_json? \\ false ) do secondary_sources = for {file, source} <- sources, @@ -171,10 +173,23 @@ defmodule Explorer.SmartContract.Solidity.Publisher do |> Map.put("name", contract_name) |> Map.put("file_path", file_name) |> Map.put("secondary_sources", secondary_sources) + |> cast_compiler_settings(is_standard_json?) publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string)) end + def cast_compiler_settings(params, false), do: Map.put(params, "compiler_settings", nil) + + def cast_compiler_settings(params, true) do + case Jason.decode(params["compiler_settings"]) do + {:ok, map} -> + Map.put(params, "compiler_settings", map) + + _ -> + Map.put(params, "compiler_settings", nil) + end + end + def publish_smart_contract(address_hash, params, abi) do attrs = address_hash |> attributes(params, abi) @@ -232,6 +247,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do defp attributes(address_hash, params, abi \\ %{}) do constructor_arguments = params["constructor_arguments"] + compiler_settings = params["compiler_settings"] clean_constructor_arguments = if constructor_arguments != nil && constructor_arguments != "" do @@ -240,6 +256,13 @@ defmodule Explorer.SmartContract.Solidity.Publisher do nil end + clean_compiler_settings = + if compiler_settings in ["", nil, %{}] do + nil + else + compiler_settings + end + prepared_external_libraries = prepare_external_libraies(params["external_libraries"]) compiler_version = CompilerVersion.get_strict_compiler_version(:solc, params["compiler_version"]) @@ -261,7 +284,9 @@ defmodule Explorer.SmartContract.Solidity.Publisher do verified_via_sourcify: params["verified_via_sourcify"], partially_verified: params["partially_verified"], is_vyper_contract: false, - autodetect_constructor_args: params["autodetect_constructor_args"] + autodetect_constructor_args: params["autodetect_constructor_args"], + is_yul: params["is_yul"] || false, + compiler_settings: clean_compiler_settings } end diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex index bf8e9e6780f6..eb849469ac58 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex @@ -51,7 +51,10 @@ defmodule Explorer.SmartContract.Solidity.Verifier do params |> Map.put("creation_bytecode", creation_tx_input) |> Map.put("deployed_bytecode", deployed_bytecode) - |> Map.put("sources", %{"#{params["name"]}.sol" => params["contract_source_code"]}) + |> Map.put("sources", %{ + "#{params["name"]}.#{smart_contract_source_file_extension(parse_boolean(params["is_yul"]))}" => + params["contract_source_code"] + }) |> Map.put("contract_libraries", params["external_libraries"]) |> Map.put("optimization_runs", prepare_optimization_runs(params["optimization"], params["optimization_runs"])) |> RustVerifierInterface.verify_multi_part() @@ -81,6 +84,9 @@ defmodule Explorer.SmartContract.Solidity.Verifier do end) end + defp smart_contract_source_file_extension(true), do: "yul" + defp smart_contract_source_file_extension(_), do: "sol" + defp prepare_optimization_runs(false_, _) when false_ in [false, "false"], do: nil defp prepare_optimization_runs(true_, runs) when true_ in [true, "true"] do @@ -199,6 +205,7 @@ defmodule Explorer.SmartContract.Solidity.Verifier do |> Map.put("file_path", file_path) |> Map.put("name", contract_name) |> Map.put("secondary_sources", secondary_sources) + |> Map.put("compiler_settings", map_json_input["settings"]) {:halt, {:ok, verified_data, additional_params}} diff --git a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex index b33870f3be50..9e1c7f40c211 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex @@ -367,6 +367,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do |> Map.put("optimization_runs", Map.get(optimizer, "runs")) |> Map.put("external_libraries", Map.get(settings, "libraries")) |> Map.put("verified_via_sourcify", true) + |> Map.put("compiler_settings", settings) %{ "params_to_publish" => params, diff --git a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex index 753b13b08b8b..07da5222d35d 100644 --- a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex @@ -172,7 +172,7 @@ defmodule Explorer.Token.InstanceMetadataRetriever do def fetch_json(%{@uri => {:ok, ["data:application/json," <> json]}}, hex_token_id) do decoded_json = URI.decode(json) - fetch_json(%{@token_uri => {:ok, [decoded_json]}}, hex_token_id) + fetch_json(%{@uri => {:ok, [decoded_json]}}, hex_token_id) rescue e -> Logger.debug(["Unknown metadata format #{inspect(json)}. error #{inspect(e)}"], @@ -182,6 +182,40 @@ defmodule Explorer.Token.InstanceMetadataRetriever do {:error, json} end + def fetch_json(%{@token_uri => {:ok, ["data:application/json;base64," <> base64_encoded_json]}}, hex_token_id) do + case Base.url_decode64(base64_encoded_json) do + {:ok, base64_decoded} -> + fetch_json(%{@token_uri => {:ok, [base64_decoded]}}, hex_token_id) + + _ -> + {:error, base64_encoded_json} + end + rescue + e -> + Logger.debug(["Unknown metadata format base64 #{inspect(base64_encoded_json)}. error #{inspect(e)}"], + fetcher: :token_instances + ) + + {:error, base64_encoded_json} + end + + def fetch_json(%{@uri => {:ok, ["data:application/json;base64," <> base64_encoded_json]}}, hex_token_id) do + case Base.url_decode64(base64_encoded_json) do + {:ok, base64_decoded} -> + fetch_json(%{@uri => {:ok, [base64_decoded]}}, hex_token_id) + + _ -> + {:error, base64_encoded_json} + end + rescue + e -> + Logger.debug(["Unknown metadata format base64 #{inspect(base64_encoded_json)}. error #{inspect(e)}"], + fetcher: :token_instances + ) + + {:error, base64_encoded_json} + end + def fetch_json(%{@token_uri => {:ok, ["ipfs://ipfs/" <> ipfs_uid]}}, hex_token_id) do ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid fetch_metadata_inner(ipfs_url, hex_token_id) diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 304b9330b68d..29b744d708a0 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -342,7 +342,10 @@ defmodule Explorer.Token.MetadataRetriever do defp handle_large_string(nil), do: nil defp handle_large_string(string), do: handle_large_string(string, byte_size(string)) - defp handle_large_string(string, size) when size > 255, do: binary_part(string, 0, 255) + + defp handle_large_string(string, size) when size > 255, + do: string |> binary_part(0, 255) |> String.chunk(:valid) |> List.first() + defp handle_large_string(string, _size), do: string defp remove_null_bytes(string) do diff --git a/apps/explorer/lib/explorer/utility/rust_service.ex b/apps/explorer/lib/explorer/utility/rust_service.ex new file mode 100644 index 000000000000..63f949961252 --- /dev/null +++ b/apps/explorer/lib/explorer/utility/rust_service.ex @@ -0,0 +1,15 @@ +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/visualize/sol2uml.ex b/apps/explorer/lib/explorer/visualize/sol2uml.ex new file mode 100644 index 000000000000..b507c63a2e10 --- /dev/null +++ b/apps/explorer/lib/explorer/visualize/sol2uml.ex @@ -0,0 +1,68 @@ +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 HTTPoison.Response + require Logger + + @post_timeout 60_000 + @request_error_msg "Error while sending request to visualizer microservice" + + def visualize_contracts(body) do + http_post_request(visualize_contracts_url(), body) + end + + def 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}} -> + proccess_visualizer_response(body) + + {:ok, %Response{body: body, status_code: status_code}} -> + Logger.error(fn -> ["Invalid status code from visualizer: #{status_code}. body: ", inspect(body)] end) + {:error, "failed to visualize contract"} + + {:error, error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to visualizer 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 + + def proccess_visualizer_response(body) when is_binary(body) do + case Jason.decode(body) do + {:ok, decoded} -> + proccess_visualizer_response(decoded) + + _ -> + {:error, body} + end + end + + def proccess_visualizer_response(%{"svg" => svg}) do + {:ok, svg} + end + + def proccess_visualizer_response(other), do: {:error, other} + + def visualize_contracts_url, do: "#{base_api_url()}" <> "/solidity:visualize-contracts" + + def base_api_url, do: "#{base_url()}" <> "/api/v1" + + def base_url do + RustService.base_url(__MODULE__) + end + + def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] +end diff --git a/apps/explorer/priv/repo/migrations/20220527131249_add_implementation_fields.exs b/apps/explorer/priv/repo/migrations/20220527131249_add_implementation_fields.exs new file mode 100644 index 000000000000..5f3839dd4a4c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20220527131249_add_implementation_fields.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Migrations.AddImplementationFields do + use Ecto.Migration + + def change do + alter table(:smart_contracts) do + add(:implementation_address_hash, :bytea, null: true) + add(:implementation_fetched_at, :"timestamp without time zone", null: true) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs b/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs index 24244fd249a7..95ad38588c15 100644 --- a/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs +++ b/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs @@ -5,11 +5,11 @@ defmodule Explorer.Repo.Migrations.AddMethodIdIndex do def up do execute(""" - CREATE INDEX CONCURRENTLY method_id ON public.transactions USING btree (substring(input for 4)); + CREATE INDEX CONCURRENTLY IF NOT EXISTS method_id ON public.transactions USING btree (substring(input for 4)); """) end def down do - execute("DROP INDEX method_id") + execute("DROP INDEX IF EXISTS method_id") end end diff --git a/apps/explorer/priv/repo/migrations/20221114113853_remove_not_null_constraint_from_abi.exs b/apps/explorer/priv/repo/migrations/20221114113853_remove_not_null_constraint_from_abi.exs new file mode 100644 index 000000000000..81903e79e30c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20221114113853_remove_not_null_constraint_from_abi.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.RemoveNotNullConstraintFromAbi do + use Ecto.Migration + + def change do + execute("ALTER TABLE smart_contracts ALTER COLUMN abi DROP NOT NULL;") + end +end diff --git a/apps/explorer/priv/repo/migrations/20221117075456_modify_address_token_balances_indexes.exs b/apps/explorer/priv/repo/migrations/20221117075456_modify_address_token_balances_indexes.exs new file mode 100644 index 000000000000..9c21c4b40a89 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20221117075456_modify_address_token_balances_indexes.exs @@ -0,0 +1,58 @@ +defmodule Explorer.Repo.Migrations.ModifyAddressTokenBalancesIndexes do + use Ecto.Migration + + def change do + drop_if_exists( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash block_number)a, + name: :fetched_token_balances, + where: "token_id IS NULL" + ) + ) + + drop_if_exists( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash token_id block_number)a, + name: :fetched_token_balances_with_token_id, + where: "token_id IS NOT NULL" + ) + ) + + create_if_not_exists( + unique_index( + :address_token_balances, + [:address_hash, :token_contract_address_hash, "COALESCE(token_id, -1)", :block_number], + name: :fetched_token_balances + ) + ) + + drop_if_exists( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash block_number)a, + name: :unfetched_token_balances, + where: "value_fetched_at IS NULL and token_id IS NULL" + ) + ) + + drop_if_exists( + unique_index( + :address_token_balances, + ~w(address_hash token_contract_address_hash token_id block_number)a, + name: :unfetched_token_balances_with_token_id, + where: "value_fetched_at IS NULL and token_id IS NOT NULL" + ) + ) + + create_if_not_exists( + unique_index( + :address_token_balances, + [:address_hash, :token_contract_address_hash, "COALESCE(token_id, -1)", :block_number], + name: :unfetched_token_balances, + where: "value_fetched_at IS NULL" + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20221117080657_modify_address_current_token_balances_indexes.exs b/apps/explorer/priv/repo/migrations/20221117080657_modify_address_current_token_balances_indexes.exs new file mode 100644 index 000000000000..295a7a5c3f15 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20221117080657_modify_address_current_token_balances_indexes.exs @@ -0,0 +1,31 @@ +defmodule Explorer.Repo.Migrations.ModifyAddressCurrentTokenBalancesIndexes do + use Ecto.Migration + + def change do + drop_if_exists( + unique_index( + :address_current_token_balances, + ~w(address_hash token_contract_address_hash)a, + name: :fetched_current_token_balances, + where: "token_id IS NULL" + ) + ) + + drop_if_exists( + unique_index( + :address_current_token_balances, + ~w(address_hash token_contract_address_hash token_id)a, + name: :fetched_current_token_balances_with_token_id, + where: "token_id IS NOT NULL" + ) + ) + + create_if_not_exists( + unique_index( + :address_current_token_balances, + [:address_hash, :token_contract_address_hash, "COALESCE(token_id, -1)"], + name: :fetched_current_token_balances + ) + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20221120184715_add_json_compiler_settings.exs b/apps/explorer/priv/repo/migrations/20221120184715_add_json_compiler_settings.exs new file mode 100644 index 000000000000..e6f90d78648a --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20221120184715_add_json_compiler_settings.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.AddJsonCompilerSettings do + use Ecto.Migration + + def change do + alter table(:smart_contracts) do + add(:compiler_settings, :jsonb, null: true) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20221126103223_add_txs_indexes.exs b/apps/explorer/priv/repo/migrations/20221126103223_add_txs_indexes.exs new file mode 100644 index 000000000000..241ed059688c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20221126103223_add_txs_indexes.exs @@ -0,0 +1,73 @@ +defmodule Explorer.Repo.Migrations.AddTxsIndexes do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + drop_if_exists( + index( + :transactions, + [:from_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash], + name: "transactions_from_address_hash_recent_collated_index", + concurrently: true + ) + ) + + drop_if_exists( + index( + :transactions, + [:to_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash], + name: "transactions_to_address_hash_recent_collated_index", + concurrently: true + ) + ) + + drop_if_exists( + index( + :transactions, + [:created_contract_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash], + name: "transactions_created_contract_address_hash_recent_collated_index", + concurrently: true + ) + ) + + create_if_not_exists( + index( + :transactions, + [ + :from_address_hash, + "block_number DESC NULLS FIRST", + "index DESC NULLS FIRST", + "inserted_at DESC", + "hash ASC" + ], + name: "transactions_from_address_hash_with_pending_index", + concurrently: true + ) + ) + + create_if_not_exists( + index( + :transactions, + [:to_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", "inserted_at DESC", "hash ASC"], + name: "transactions_to_address_hash_with_pending_index", + concurrently: true + ) + ) + + create_if_not_exists( + index( + :transactions, + [ + :created_contract_address_hash, + "block_number DESC NULLS FIRST", + "index DESC NULLS FIRST", + "inserted_at DESC", + "hash ASC" + ], + name: "transactions_created_contract_address_hash_with_pending_index", + concurrently: true + ) + ) + end +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 7c34c72719cd..31eba0553de0 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 @@ -97,30 +97,30 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do %Explorer.Chain.Address.CurrentTokenBalance{ address_hash: ^address_hash, block_number: ^block_number, - token_contract_address_hash: ^token_erc_20_contract_address_hash, - value: ^value_3, - token_id: ^token_id_3 + token_contract_address_hash: ^token_contract_address_hash, + value: ^value_1, + token_id: ^token_id_1 }, %Explorer.Chain.Address.CurrentTokenBalance{ address_hash: ^address_hash, block_number: ^block_number, - token_contract_address_hash: ^token_erc_721_contract_address_hash, - value: ^value_5, - token_id: nil + token_contract_address_hash: ^token_contract_address_hash, + value: ^value_2, + token_id: ^token_id_2 }, %Explorer.Chain.Address.CurrentTokenBalance{ address_hash: ^address_hash, block_number: ^block_number, - token_contract_address_hash: ^token_contract_address_hash, - value: ^value_1, - token_id: ^token_id_1 + token_contract_address_hash: ^token_erc_20_contract_address_hash, + value: ^value_3, + token_id: ^token_id_3 }, %Explorer.Chain.Address.CurrentTokenBalance{ address_hash: ^address_hash, block_number: ^block_number, - token_contract_address_hash: ^token_contract_address_hash, - value: ^value_2, - token_id: ^token_id_2 + token_contract_address_hash: ^token_erc_721_contract_address_hash, + value: ^value_5, + token_id: nil } ], address_current_token_balances_update_token_holder_counts: [ @@ -172,7 +172,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do block_number: block_number, token_contract_address_hash: token_erc_721.contract_address_hash, value: value_4, - value_fetched_at: DateTime.utc_now(), + value_fetched_at: DateTime.add(DateTime.utc_now(), -1), token_id: token_id_4, token_type: "ERC-721" }, diff --git a/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs index f44b6bca792f..15725869bd85 100644 --- a/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs @@ -41,6 +41,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do address_hash: ^address_hash, block_number: ^block_number, token_contract_address_hash: ^token_contract_address_hash, + token_id: nil, value: ^value, value_fetched_at: ^value_fetched_at } @@ -83,6 +84,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do address_hash: address_hash, block_number: ^block_number, token_contract_address_hash: ^token_contract_address_hash, + token_id: nil, value: nil, value_fetched_at: ^value_fetched_at } @@ -153,6 +155,70 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do run_changes(new_changes, options) end + test "set value_fetched_at to null for existing record if incoming data has this field empty" do + address = insert(:address) + token = insert(:token) + + options = %{ + timeout: :infinity, + timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + } + + block_number = 1 + + value = Decimal.new(100) + value_fetched_at = DateTime.utc_now() + + token_contract_address_hash = token.contract_address_hash + address_hash = address.hash + + first_changes = %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash, + token_id: 11, + token_type: "ERC-721", + value: value, + value_fetched_at: value_fetched_at + } + + assert {:ok, + %{ + address_token_balances: [ + %TokenBalance{ + address_hash: address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_contract_address_hash, + token_id: nil, + value: ^value, + value_fetched_at: ^value_fetched_at + } + ] + }} = run_changes(first_changes, options) + + second_changes = %{ + address_hash: address_hash, + block_number: block_number, + token_contract_address_hash: token_contract_address_hash, + token_id: 12, + token_type: "ERC-721" + } + + assert {:ok, + %{ + address_token_balances: [ + %TokenBalance{ + address_hash: address_hash, + block_number: ^block_number, + token_contract_address_hash: ^token_contract_address_hash, + token_id: nil, + value: ^value, + value_fetched_at: nil + } + ] + }} = run_changes(second_changes, options) + end + defp run_changes(changes, options) when is_map(changes) do run_changes_list([changes], options) end diff --git a/apps/explorer/test/explorer/chain/smart_contract_test.exs b/apps/explorer/test/explorer/chain/smart_contract_test.exs new file mode 100644 index 000000000000..38ff09e3b1e3 --- /dev/null +++ b/apps/explorer/test/explorer/chain/smart_contract_test.exs @@ -0,0 +1,237 @@ +defmodule Explorer.Chain.SmartContractTest do + use Explorer.DataCase, async: false + + import Mox + alias Explorer.Chain + alias Explorer.Chain.SmartContract + + doctest Explorer.Chain.SmartContract + + setup :verify_on_exit! + setup :set_mox_global + + describe "test fetching implementation" 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)) + + refute smart_contract.implementation_fetched_at + + # fetch nil implementation and 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) + verify!(EthereumJSONRPC.Mox) + assert_empty_implementation(smart_contract.address_hash) + + Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0) + + get_eip1967_implementation_error_response() + refute SmartContract.proxy_contract?(smart_contract) + verify!(EthereumJSONRPC.Mox) + + get_eip1967_implementation_non_zero_address() + assert SmartContract.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) + 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) + + Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0) + get_eip1967_implementation_non_zero_address() + assert SmartContract.proxy_contract?(smart_contract) + verify!(EthereumJSONRPC.Mox) + + get_eip1967_implementation_error_response() + assert SmartContract.proxy_contract?(smart_contract) + verify!(EthereumJSONRPC.Mox) + end + + test "test get_implementation_adddress_hash/1" 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)) + + refute smart_contract.implementation_fetched_at + + # fetch nil implementation and 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) + + # extract proxy info from db + assert {nil, nil} = SmartContract.get_implementation_address_hash(smart_contract) + assert_empty_implementation(smart_contract.address_hash) + + Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0) + + 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) + + assert {^string_implementation_address_hash, "proxy"} = + SmartContract.get_implementation_address_hash(smart_contract) + + verify!(EthereumJSONRPC.Mox) + + assert_exact_name_and_address( + smart_contract.address_hash, + implementation_smart_contract.address_hash, + implementation_smart_contract.name + ) + + get_eip1967_implementation_error_response() + + assert {^string_implementation_address_hash, "proxy"} = + SmartContract.get_implementation_address_hash(smart_contract) + + verify!(EthereumJSONRPC.Mox) + + assert_exact_name_and_address( + smart_contract.address_hash, + implementation_smart_contract.address_hash, + implementation_smart_contract.name + ) + + contract_1 = Chain.address_hash_to_smart_contract(smart_contract.address_hash) + + Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20)) + + 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) + + 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) + 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) + end + 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) + 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 + + def get_eip1967_implementation_error_response do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:error, "error"} + end) + end + + def assert_empty_implementation(address_hash) do + contract = Chain.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_address(address_hash) do + contract = Chain.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) + 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) + assert contract.implementation_fetched_at + assert contract.implementation_name == implementation_name + assert to_string(contract.implementation_address_hash) == to_string(implementation_address_hash) + end +end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 9808527a8da4..e191c081b843 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -3546,7 +3546,7 @@ defmodule Explorer.ChainTest do assert [ %TokenTransfer{ - token: %Ecto.Association.NotLoaded{}, + token: %Token{}, transaction: %Ecto.Association.NotLoaded{} } ] = Chain.transaction_to_token_transfers(transaction.hash) @@ -6140,26 +6140,36 @@ defmodule Explorer.ChainTest do 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(proxy_contract_address, nil) == [] + + 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(proxy_contract_address, []) == [] + 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) - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - assert Chain.combine_proxy_implementation_abi(proxy_contract_address, @proxy_abi) == @proxy_abi + + 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) - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") implementation_contract_address = insert(:contract_address) @@ -6187,7 +6197,7 @@ defmodule Explorer.ChainTest do end ) - combined_abi = Chain.combine_proxy_implementation_abi(proxy_contract_address.hash, @proxy_abi) + 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 @@ -6197,26 +6207,36 @@ defmodule Explorer.ChainTest do 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(proxy_contract_address, nil) == [] + + 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(proxy_contract_address, []) == [] + 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) - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - assert Chain.get_implementation_abi_from_proxy(proxy_contract_address, @proxy_abi) == [] + + 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) - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") implementation_contract_address = insert(:contract_address) @@ -6244,14 +6264,16 @@ defmodule Explorer.ChainTest do end ) - implementation_abi = Chain.get_implementation_abi_from_proxy(proxy_contract_address.hash, @proxy_abi) + 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) - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") + + smart_contract = + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") implementation_contract_address = insert(:contract_address) @@ -6281,7 +6303,7 @@ defmodule Explorer.ChainTest do end ) - implementation_abi = Chain.get_implementation_abi_from_proxy(proxy_contract_address.hash, []) + implementation_abi = Chain.get_implementation_abi_from_proxy(smart_contract) assert implementation_abi == @implementation_abi end diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs index 9f8ead7468d8..404ae713bb9f 100644 --- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs @@ -340,6 +340,58 @@ defmodule Explorer.Token.MetadataRetrieverTest do assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected end + test "shortens strings larger than 255 characters with unicode graphemes" do + long_token_name_shortened = + "文章の論旨や要点を短くまとめて表現する要約文。学生の頃、レポート作成などで書いた経験があるものの、それ以降はまったく書いていないという人は多いことでしょう。 しかし、文章" + + token = insert(:token, contract_address: build(:contract_address)) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + 1, + fn requests, _opts -> + {:ok, + Enum.map(requests, fn + %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000012" + } + + %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000128e69687e7aba0e381aee8ab96e697a8e38284e8a681e782b9e38292e79fade3818fe381bee381a8e38281e381a6e8a1a8e78fbee38199e3828be8a681e7b484e69687e38082e5ada6e7949fe381aee9a083e38081e383ace3839de383bce38388e4bd9ce68890e381aae381a9e381a7e69bb8e38184e3819fe7b58ce9a893e3818ce38182e3828be38282e381aee381aee38081e3819de3828ce4bba5e9998de381afe381bee381a3e3819fe3818fe69bb8e38184e381a6e38184e381aae38184e381a8e38184e38186e4babae381afe5a49ae38184e38193e381a8e381a7e38197e38287e38186e380822020e38197e3818be38197e38081e69687e7aba0e4bd9ce68890e3818ce88ba6e6898be381aae4babae38284e38081e69687e7aba0e3818ce3828fe3818b000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} -> + %{ + id: id, + result: + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000" + } + + %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + end)} + end + ) + + expected = %{ + name: long_token_name_shortened, + decimals: 18, + total_supply: 1_000_000_000_000_000_000, + symbol: "BNT" + } + + assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected + end + test "retries when some function gave error" do token = insert(:token, contract_address: build(:contract_address)) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index e83166edd714..80a10f5811cb 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -180,6 +180,13 @@ defmodule Explorer.Factory do } end + def unique_address_name_factory do + %Address.Name{ + address: build(:address), + name: sequence("FooContract") + } + end + def unfetched_balance_factory do %CoinBalance{ address_hash: address_hash(), @@ -667,6 +674,10 @@ defmodule Explorer.Factory do } end + def unique_token_factory do + Map.replace(token_factory(), :name, sequence("Infinite Token")) + end + def token_transfer_log_factory do token_contract_address = build(:address) to_address = build(:address) @@ -835,6 +846,10 @@ defmodule Explorer.Factory do } end + def unique_smart_contract_factory do + Map.replace(smart_contract_factory(), :name, sequence("SimpleStorage")) + end + def decompiled_smart_contract_factory do contract_code_info = contract_code_info() @@ -865,6 +880,15 @@ defmodule Explorer.Factory do } end + def address_coin_balance_factory do + %CoinBalance{ + address: insert(:address), + block_number: insert(:block).number, + value: Enum.random(1..100_000_000), + value_fetched_at: DateTime.utc_now() + } + end + def address_current_token_balance_factory do %CurrentTokenBalance{ address: build(:address), @@ -876,6 +900,17 @@ defmodule Explorer.Factory do } end + def address_current_token_balance_with_token_id_factory do + %CurrentTokenBalance{ + address: build(:address), + token_contract_address_hash: insert(:token).contract_address_hash, + block_number: block_number(), + value: Enum.random(1..100_000), + value_fetched_at: DateTime.utc_now(), + token_id: Enum.random([nil, Enum.random(1..100_000)]) + } + end + defp block_hash_to_next_transaction_index(block_hash) do import Kernel, except: [+: 2] diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance.ex b/apps/indexer/lib/indexer/fetcher/coin_balance.ex index 1fd0624e6720..3321b9016336 100644 --- a/apps/indexer/lib/indexer/fetcher/coin_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/coin_balance.ex @@ -20,13 +20,8 @@ defmodule Indexer.Fetcher.CoinBalance do use BufferedTask - @defaults [ - flush_interval: :timer.seconds(3), - max_batch_size: 500, - max_concurrency: 4, - task_supervisor: Indexer.Fetcher.CoinBalance.TaskSupervisor, - metadata: [fetcher: :coin_balance] - ] + @default_max_batch_size 500 + @default_max_concurrency 4 @doc """ Asynchronously fetches balances for each address `hash` at the `block_number`. @@ -56,7 +51,7 @@ defmodule Indexer.Fetcher.CoinBalance do end merged_init_options = - @defaults + defaults() |> Keyword.merge(mergeable_init_options) |> Keyword.put(:state, state) @@ -264,4 +259,14 @@ 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/token_instance.ex b/apps/indexer/lib/indexer/fetcher/token_instance.ex index 1dbf68d87045..278506b91b13 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance.ex @@ -54,7 +54,6 @@ defmodule Indexer.Fetcher.TokenInstance do @impl BufferedTask def run([%{contract_address_hash: hash, token_id: token_id}], _json_rpc_named_arguments) do fetch_instance(hash, token_id) - update_current_token_balances(hash, token_id) :ok end @@ -99,58 +98,6 @@ defmodule Indexer.Fetcher.TokenInstance do end end - defp update_current_token_balances(token_contract_address_hash, token_id) do - token_id - |> instance_owner_request(token_contract_address_hash) - |> List.wrap() - |> InstanceOwnerReader.get_owner_of() - |> Enum.map(¤t_token_balances_import_params/1) - |> all_import_params() - |> Chain.import() - end - - defp instance_owner_request(token_id, token_contract_address_hash) do - %{ - token_contract_address_hash: to_string(token_contract_address_hash), - token_id: Decimal.to_integer(token_id) - } - end - - defp current_token_balances_import_params(%{token_contract_address_hash: hash, token_id: token_id, owner: owner}) do - %{ - value: Decimal.new(1), - block_number: BlockNumber.get_max(), - value_fetched_at: DateTime.utc_now(), - token_id: token_id, - token_type: Repo.get_by(Token, contract_address_hash: hash).type, - address_hash: owner, - token_contract_address_hash: hash - } - end - - defp all_import_params(balances_import_params) do - addresses_import_params = - balances_import_params - |> Enum.reduce([], fn %{address_hash: address_hash}, acc -> - case Repo.get_by(Address, hash: address_hash) do - nil -> [%{hash: address_hash} | acc] - _address -> acc - end - end) - |> case do - [] -> %{} - params -> %{addresses: %{params: params}} - end - - current_token_balances_import_params = %{ - address_current_token_balances: %{ - params: balances_import_params - } - } - - Map.merge(current_token_balances_import_params, addresses_import_params) - end - @doc """ Fetches token instance data asynchronously. """ diff --git a/apps/indexer/test/indexer/fetcher/token_instance_test.exs b/apps/indexer/test/indexer/fetcher/token_instance_test.exs deleted file mode 100644 index ade5894ac5a2..000000000000 --- a/apps/indexer/test/indexer/fetcher/token_instance_test.exs +++ /dev/null @@ -1,87 +0,0 @@ -defmodule Indexer.Fetcher.TokenInstanceTest do - use EthereumJSONRPC.Case, async: false - use Explorer.DataCase - - import Mox - - alias Explorer.Chain - alias Explorer.Chain.Address - alias Explorer.Chain.Address.CurrentTokenBalance - alias Explorer.Repo - alias Indexer.Fetcher.TokenInstance - - describe "run/2" do - test "updates current token balance" do - token = insert(:token, type: "ERC-1155") - token_contract_address_hash = token.contract_address_hash - instance = insert(:token_instance, token_contract_address_hash: token_contract_address_hash) - token_id = instance.token_id - address = insert(:address, hash: "0x57e93bb58268de818b42e3795c97bad58afcd3fe") - address_hash = address.hash - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0xc87b56dd" <> _}, _]}], _ -> - {:ok, - [ - %{ - id: 0, - result: - "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000027b7d000000000000000000000000000000000000000000000000000000000000" - } - ]} - end) - |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0x6352211e" <> _}, _]}], _ -> - {:ok, [%{id: 0, result: "0x00000000000000000000000057e93bb58268de818b42e3795c97bad58afcd3fe"}]} - end) - - TokenInstance.run( - [%{contract_address_hash: token_contract_address_hash, token_id: token_id}], - nil - ) - - assert %{ - token_id: ^token_id, - token_type: "ERC-1155", - token_contract_address_hash: ^token_contract_address_hash, - address_hash: ^address_hash - } = Repo.one(CurrentTokenBalance) - end - - test "updates current token balance with missing address" do - token = insert(:token, type: "ERC-1155") - token_contract_address_hash = token.contract_address_hash - instance = insert(:token_instance, token_contract_address_hash: token_contract_address_hash) - token_id = instance.token_id - {:ok, address_hash} = Chain.string_to_address_hash("0x57e93bb58268de818b42e3795c97bad58afcd3fe") - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0xc87b56dd" <> _}, _]}], _ -> - {:ok, - [ - %{ - id: 0, - result: - "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000027b7d000000000000000000000000000000000000000000000000000000000000" - } - ]} - end) - |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0x6352211e" <> _}, _]}], _ -> - {:ok, [%{id: 0, result: "0x00000000000000000000000057e93bb58268de818b42e3795c97bad58afcd3fe"}]} - end) - - TokenInstance.run( - [%{contract_address_hash: token_contract_address_hash, token_id: token_id}], - nil - ) - - assert %{ - token_id: ^token_id, - token_type: "ERC-1155", - token_contract_address_hash: ^token_contract_address_hash, - address_hash: ^address_hash - } = Repo.one(CurrentTokenBalance) - - assert %Address{} = Repo.get_by(Address, hash: address_hash) - end - end -end diff --git a/config/runtime.exs b/config/runtime.exs index 3db22bf1d93c..9e54ebd200f8 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -9,12 +9,12 @@ indexer_memory_limit = |> System.get_env(to_string(indexer_memory_limit_default)) |> Integer.parse() |> case do - {integer, ""} -> integer - _ -> indexer_memory_limit_default - end + {integer, ""} -> integer + _ -> indexer_memory_limit_default + end config :indexer, - memory_limit: indexer_memory_limit <<< 32 + memory_limit: indexer_memory_limit <<< 32 indexer_empty_blocks_sanitizer_batch_size_default = 100 @@ -23,20 +23,20 @@ indexer_empty_blocks_sanitizer_batch_size = |> System.get_env(to_string(indexer_empty_blocks_sanitizer_batch_size_default)) |> Integer.parse() |> case do - {integer, ""} -> integer - _ -> indexer_empty_blocks_sanitizer_batch_size_default - end + {integer, ""} -> integer + _ -> indexer_empty_blocks_sanitizer_batch_size_default + end config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer.Supervisor, - disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true" + disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true" config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, batch_size: indexer_empty_blocks_sanitizer_batch_size config :block_scout_web, :footer, - chat_link: System.get_env("FOOTER_CHAT_LINK", "http://discord.gg/celo"), - forum_link: System.get_env("FOOTER_FORUM_LINK", "https://forum.celo.org/"), - github_link: System.get_env("FOOTER_GITHUB_LINK", "https://github.com/celo-org/blockscout"), - enable_forum_link: System.get_env("FOOTER_ENABLE_FORUM_LINK", "false") == "true" + chat_link: System.get_env("FOOTER_CHAT_LINK", "http://discord.gg/celo"), + forum_link: System.get_env("FOOTER_FORUM_LINK", "https://forum.celo.org/"), + github_link: System.get_env("FOOTER_GITHUB_LINK", "https://github.com/celo-org/blockscout"), + enable_forum_link: System.get_env("FOOTER_ENABLE_FORUM_LINK", "false") == "true" ###################### ### BlockScout Web ### @@ -44,34 +44,34 @@ config :block_scout_web, :footer, # Configures Ueberauth's Auth0 auth provider config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, - domain: System.get_env("ACCOUNT_AUTH0_DOMAIN"), - client_id: System.get_env("ACCOUNT_AUTH0_CLIENT_ID"), - client_secret: System.get_env("ACCOUNT_AUTH0_CLIENT_SECRET") + domain: System.get_env("ACCOUNT_AUTH0_DOMAIN"), + client_id: System.get_env("ACCOUNT_AUTH0_CLIENT_ID"), + client_secret: System.get_env("ACCOUNT_AUTH0_CLIENT_SECRET") # Configures Ueberauth local settings config :ueberauth, Ueberauth, - logout_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_URL"), - logout_return_to_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_RETURN_URL") + logout_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_URL"), + logout_return_to_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_RETURN_URL") config :block_scout_web, - version: System.get_env("BLOCKSCOUT_VERSION"), - segment_key: System.get_env("SEGMENT_KEY"), - release_link: System.get_env("RELEASE_LINK"), - decompiled_smart_contract_token: System.get_env("DECOMPILED_SMART_CONTRACT_TOKEN"), - show_percentage: if(System.get_env("SHOW_ADDRESS_MARKETCAP_PERCENTAGE", "true") == "false", do: false, else: true), - checksum_address_hashes: if(System.get_env("CHECKSUM_ADDRESS_HASHES", "true") == "false", do: false, else: true) + version: System.get_env("BLOCKSCOUT_VERSION"), + segment_key: System.get_env("SEGMENT_KEY"), + release_link: System.get_env("RELEASE_LINK"), + decompiled_smart_contract_token: System.get_env("DECOMPILED_SMART_CONTRACT_TOKEN"), + show_percentage: if(System.get_env("SHOW_ADDRESS_MARKETCAP_PERCENTAGE", "true") == "false", do: false, else: true), + checksum_address_hashes: if(System.get_env("CHECKSUM_ADDRESS_HASHES", "true") == "false", do: false, else: true) config :block_scout_web, BlockScoutWeb.Chain, - network: System.get_env("NETWORK"), - subnetwork: System.get_env("SUBNETWORK"), - network_icon: System.get_env("NETWORK_ICON"), - logo: System.get_env("LOGO", "/images/celo_logo.svg"), - logo_footer: System.get_env("LOGO_FOOTER", "/images/celo_logo.svg"), - logo_text: System.get_env("LOGO_TEXT"), - has_emission_funds: false, - show_maintenance_alert: System.get_env("SHOW_MAINTENANCE_ALERT", "false") == "true", - enable_testnet_label: System.get_env("SHOW_TESTNET_LABEL", "false") == "true", - testnet_label_text: System.get_env("TESTNET_LABEL_TEXT", "Testnet") + network: System.get_env("NETWORK"), + subnetwork: System.get_env("SUBNETWORK"), + network_icon: System.get_env("NETWORK_ICON"), + logo: System.get_env("LOGO", "/images/celo_logo.svg"), + logo_footer: System.get_env("LOGO_FOOTER", "/images/celo_logo.svg"), + logo_text: System.get_env("LOGO_TEXT"), + has_emission_funds: false, + show_maintenance_alert: System.get_env("SHOW_MAINTENANCE_ALERT", "false") == "true", + enable_testnet_label: System.get_env("SHOW_TESTNET_LABEL", "false") == "true", + testnet_label_text: System.get_env("TESTNET_LABEL_TEXT", "Testnet") verification_max_libraries_default = 10 @@ -80,38 +80,38 @@ verification_max_libraries = |> System.get_env(to_string(verification_max_libraries_default)) |> Integer.parse() |> case do - {integer, ""} -> integer - _ -> verification_max_libraries_default - end + {integer, ""} -> integer + _ -> verification_max_libraries_default + end config :block_scout_web, - link_to_other_explorers: System.get_env("LINK_TO_OTHER_EXPLORERS") == "true", - other_explorers: System.get_env("OTHER_EXPLORERS"), - webapp_url: System.get_env("WEBAPP_URL"), - api_url: System.get_env("API_URL"), - apps_menu: if(System.get_env("APPS_MENU", "false") == "true", do: true, else: false), - stats_enabled: System.get_env("DISABLE_STATS") != "true", - stats_report_url: System.get_env("STATS_REPORT_URL", ""), - makerdojo_url: System.get_env("MAKERDOJO_URL", ""), - apps: System.get_env("APPS") || System.get_env("EXTERNAL_APPS"), - gas_price: System.get_env("GAS_PRICE", nil), - restricted_list: System.get_env("RESTRICTED_LIST", nil), - restricted_list_key: System.get_env("RESTRICTED_LIST_KEY", nil), - dark_forest_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST"), - dark_forest_addresses_v_0_5: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST_V_0_5"), - circles_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_CIRCLES"), - test_tokens_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_TEST_TOKEN"), - max_size_to_show_array_as_is: Integer.parse(System.get_env("MAX_SIZE_UNLESS_HIDE_ARRAY", "50")), - max_length_to_show_string_without_trimming: System.get_env("MAX_STRING_LENGTH_WITHOUT_TRIMMING", "2040"), - re_captcha_site_key: System.get_env("RE_CAPTCHA_SITE_KEY", nil), - re_captcha_api_key: System.get_env("RE_CAPTCHA_API_KEY", nil), - re_captcha_secret_key: System.get_env("RE_CAPTCHA_SECRET_KEY", nil), - re_captcha_project_id: System.get_env("RE_CAPTCHA_PROJECT_ID", nil), - re_captcha_client_key: System.get_env("RE_CAPTCHA_CLIENT_KEY", nil), - new_tags: System.get_env("NEW_TAGS"), - chain_id: System.get_env("CHAIN_ID"), - json_rpc: System.get_env("JSON_RPC"), - verification_max_libraries: verification_max_libraries + link_to_other_explorers: System.get_env("LINK_TO_OTHER_EXPLORERS") == "true", + other_explorers: System.get_env("OTHER_EXPLORERS"), + webapp_url: System.get_env("WEBAPP_URL"), + api_url: System.get_env("API_URL"), + apps_menu: if(System.get_env("APPS_MENU", "false") == "true", do: true, else: false), + stats_enabled: System.get_env("DISABLE_STATS") != "true", + stats_report_url: System.get_env("STATS_REPORT_URL", ""), + makerdojo_url: System.get_env("MAKERDOJO_URL", ""), + apps: System.get_env("APPS") || System.get_env("EXTERNAL_APPS"), + gas_price: System.get_env("GAS_PRICE", nil), + restricted_list: System.get_env("RESTRICTED_LIST", nil), + restricted_list_key: System.get_env("RESTRICTED_LIST_KEY", nil), + dark_forest_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST"), + dark_forest_addresses_v_0_5: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST_V_0_5"), + circles_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_CIRCLES"), + test_tokens_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_TEST_TOKEN"), + max_size_to_show_array_as_is: Integer.parse(System.get_env("MAX_SIZE_UNLESS_HIDE_ARRAY", "50")), + max_length_to_show_string_without_trimming: System.get_env("MAX_STRING_LENGTH_WITHOUT_TRIMMING", "2040"), + re_captcha_site_key: System.get_env("RE_CAPTCHA_SITE_KEY", nil), + re_captcha_api_key: System.get_env("RE_CAPTCHA_API_KEY", nil), + re_captcha_secret_key: System.get_env("RE_CAPTCHA_SECRET_KEY", nil), + re_captcha_project_id: System.get_env("RE_CAPTCHA_PROJECT_ID", nil), + re_captcha_client_key: System.get_env("RE_CAPTCHA_CLIENT_KEY", nil), + new_tags: System.get_env("NEW_TAGS"), + chain_id: System.get_env("CHAIN_ID"), + json_rpc: System.get_env("JSON_RPC"), + verification_max_libraries: verification_max_libraries default_api_rate_limit = 50 default_api_rate_limit_str = Integer.to_string(default_api_rate_limit) @@ -121,41 +121,41 @@ global_api_rate_limit_value = |> System.get_env(default_api_rate_limit_str) |> Integer.parse() |> case do - {integer, ""} -> integer - _ -> default_api_rate_limit - end + {integer, ""} -> integer + _ -> default_api_rate_limit + end api_rate_limit_by_key_value = "API_RATE_LIMIT_BY_KEY" |> System.get_env(default_api_rate_limit_str) |> Integer.parse() |> case do - {integer, ""} -> integer - _ -> default_api_rate_limit - end + {integer, ""} -> integer + _ -> default_api_rate_limit + end api_rate_limit_by_ip_value = "API_RATE_LIMIT_BY_IP" |> System.get_env(default_api_rate_limit_str) |> Integer.parse() |> case do - {integer, ""} -> integer - _ -> default_api_rate_limit - end + {integer, ""} -> integer + _ -> default_api_rate_limit + end config :block_scout_web, :api_rate_limit, - global_limit: global_api_rate_limit_value, - limit_by_key: api_rate_limit_by_key_value, - limit_by_ip: api_rate_limit_by_ip_value, - static_api_key: System.get_env("API_RATE_LIMIT_STATIC_API_KEY", nil), - whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS", nil) + global_limit: global_api_rate_limit_value, + limit_by_key: api_rate_limit_by_key_value, + limit_by_ip: api_rate_limit_by_ip_value, + static_api_key: System.get_env("API_RATE_LIMIT_STATIC_API_KEY", nil), + whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS", nil) config :block_scout_web, BlockScoutWeb.Endpoint, - server: true, - url: [ - scheme: System.get_env("BLOCKSCOUT_PROTOCOL") || "http", - host: System.get_env("BLOCKSCOUT_HOST") || "localhost" - ] + server: true, + url: [ + scheme: System.get_env("BLOCKSCOUT_PROTOCOL") || "http", + host: System.get_env("BLOCKSCOUT_HOST") || "localhost" + ] # Configures History price_chart_config = @@ -173,11 +173,11 @@ tx_chart_config = end config :block_scout_web, - chart_config: Map.merge(price_chart_config, tx_chart_config) + chart_config: Map.merge(price_chart_config, tx_chart_config) config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance, - # days - coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10") + # days + coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10") config :block_scout_web, BlockScoutWeb.API.V2, enabled: System.get_env("API_V2_ENABLED") == "true" @@ -186,15 +186,15 @@ config :block_scout_web, BlockScoutWeb.API.V2, enabled: System.get_env("API_V2_E ######################## config :ethereum_jsonrpc, - rpc_transport: if(System.get_env("ETHEREUM_JSONRPC_TRANSPORT", "http") == "http", do: :http, else: :ipc), - ipc_path: System.get_env("IPC_PATH"), - disable_archive_balances?: System.get_env("ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES", "false") == "true" + rpc_transport: if(System.get_env("ETHEREUM_JSONRPC_TRANSPORT", "http") == "http", do: :http, else: :ipc), + ipc_path: System.get_env("IPC_PATH"), + disable_archive_balances?: System.get_env("ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES", "false") == "true" debug_trace_transaction_timeout = System.get_env("ETHEREUM_JSONRPC_DEBUG_TRACE_TRANSACTION_TIMEOUT", "900s") config :ethereum_jsonrpc, :internal_transaction_timeout, debug_trace_transaction_timeout config :ethereum_jsonrpc, EthereumJSONRPC.PendingTransaction, - type: System.get_env("ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE", "default") + type: System.get_env("ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE", "default") ################ ### Explorer ### @@ -210,49 +210,57 @@ healthy_blocks_period = end config :explorer, - coin: System.get_env("COIN", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO", - coin_name: System.get_env("COIN_NAME", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO", - allowed_evm_versions: - System.get_env("ALLOWED_EVM_VERSIONS") || - "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,default", - include_uncles_in_average_block_time: - if(System.get_env("UNCLES_IN_AVERAGE_BLOCK_TIME") == "true", do: true, else: false), - healthy_blocks_period: healthy_blocks_period, - realtime_events_sender: - if(disable_webapp != "true", - do: Explorer.Chain.Events.SimpleSender, - else: Explorer.Chain.Events.PubSubSender - ) + coin: System.get_env("COIN", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO", + coin_name: System.get_env("COIN_NAME", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO", + allowed_evm_versions: + System.get_env("ALLOWED_EVM_VERSIONS") || + "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,default", + include_uncles_in_average_block_time: + if(System.get_env("UNCLES_IN_AVERAGE_BLOCK_TIME") == "true", do: true, else: false), + healthy_blocks_period: healthy_blocks_period, + realtime_events_sender: + if(disable_webapp != "true", + do: Explorer.Chain.Events.SimpleSender, + else: Explorer.Chain.Events.PubSubSender + ), + 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) + +config :explorer, Explorer.Visualize.Sol2uml, + service_url: System.get_env("VISUALIZE_SOL2UML_SERVICE_URL"), + enabled: System.get_env("VISUALIZE_SOL2UML_ENABLED") == "true" config :explorer, Explorer.Chain.Events.Listener, - enabled: - if(disable_webapp == "true" && disable_indexer == "true", - do: false, - else: true - ), - event_source: Explorer.Chain.Events.PubSubSource + enabled: + if(disable_webapp == "true" && disable_indexer == "true", + do: false, + else: true + ), + event_source: Explorer.Chain.Events.PubSubSource config :explorer, Explorer.ChainSpec.GenesisData, - chain_spec_path: - System.get_env( - "CHAIN_SPEC_PATH", - "https://www.googleapis.com/storage/v1/b/genesis_blocks/o/#{String.downcase(System.get_env("SUBNETWORK", "Baklava"))}?alt=media" - ), - emission_format: System.get_env("EMISSION_FORMAT", "DEFAULT"), - rewards_contract_address: System.get_env("REWARDS_CONTRACT", "0xeca443e8e1ab29971a45a9c57a6a9875701698a5") + chain_spec_path: + System.get_env( + "CHAIN_SPEC_PATH", + "https://www.googleapis.com/storage/v1/b/genesis_blocks/o/#{String.downcase(System.get_env("SUBNETWORK", "Baklava"))}?alt=media" + ), + emission_format: System.get_env("EMISSION_FORMAT", "DEFAULT"), + rewards_contract_address: System.get_env("REWARDS_CONTRACT", "0xeca443e8e1ab29971a45a9c57a6a9875701698a5") config :explorer, Explorer.Chain.Cache.BlockNumber, - ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), - global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) + ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), + global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) address_sum_global_ttl = "CACHE_ADDRESS_SUM_PERIOD" |> System.get_env("") |> Integer.parse() |> case do - {integer, ""} -> integer - _ -> 3600 - end + {integer, ""} -> integer + _ -> 3600 + end |> :timer.seconds() config :explorer, Explorer.Chain.Cache.AddressSum, global_ttl: address_sum_global_ttl @@ -260,12 +268,12 @@ config :explorer, Explorer.Chain.Cache.AddressSum, global_ttl: address_sum_globa config :explorer, Explorer.Chain.Cache.AddressSumMinusBurnt, global_ttl: address_sum_global_ttl config :explorer, Explorer.ExchangeRates, - store: :ets, - enabled: System.get_env("DISABLE_EXCHANGE_RATES") != "true", - coingecko_coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"), - coingecko_api_key: System.get_env("EXCHANGE_RATES_COINGECKO_API_KEY"), - coinmarketcap_api_key: System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY"), - fetch_btc_value: System.get_env("EXCHANGE_RATES_FETCH_BTC_VALUE") == "true" + store: :ets, + enabled: System.get_env("DISABLE_EXCHANGE_RATES") != "true", + coingecko_coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"), + coingecko_api_key: System.get_env("EXCHANGE_RATES_COINGECKO_API_KEY"), + coinmarketcap_api_key: System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY"), + fetch_btc_value: System.get_env("EXCHANGE_RATES_FETCH_BTC_VALUE") == "true" exchange_rates_source = cond do @@ -292,9 +300,9 @@ txs_stats_days_to_compile_at_init = |> elem(0) config :explorer, Explorer.Chain.Transaction.History.Historian, - enabled: System.get_env("ENABLE_TXS_STATS", "true") != "false", - init_lag: txs_stats_init_lag, - days_to_compile_at_init: txs_stats_days_to_compile_at_init + enabled: System.get_env("ENABLE_TXS_STATS", "true") != "false", + init_lag: txs_stats_init_lag, + days_to_compile_at_init: txs_stats_days_to_compile_at_init history_fetch_interval = case Integer.parse(System.get_env("HISTORY_FETCH_INTERVAL", "")) do @@ -307,8 +315,8 @@ config :explorer, Explorer.History.Process, history_fetch_interval: history_fetc if System.get_env("METADATA_CONTRACT") && System.get_env("VALIDATORS_CONTRACT") do config :explorer, Explorer.Validator.MetadataRetriever, - metadata_contract_address: System.get_env("METADATA_CONTRACT"), - validators_contract_address: System.get_env("VALIDATORS_CONTRACT") + metadata_contract_address: System.get_env("METADATA_CONTRACT"), + validators_contract_address: System.get_env("VALIDATORS_CONTRACT") config :explorer, Explorer.Validator.MetadataProcessor, enabled: disable_indexer != "true" else @@ -316,8 +324,8 @@ else end config :explorer, Explorer.Chain.Block.Reward, - validators_contract_address: System.get_env("VALIDATORS_CONTRACT"), - keys_manager_contract_address: System.get_env("KEYS_MANAGER_CONTRACT") + validators_contract_address: System.get_env("VALIDATORS_CONTRACT"), + keys_manager_contract_address: System.get_env("KEYS_MANAGER_CONTRACT") case System.get_env("SUPPLY_MODULE") do "rsk" -> @@ -336,57 +344,61 @@ case System.get_env("MARKET_CAP_ENABLED", "false") do end config :explorer, - checksum_function: System.get_env("CHECKSUM_FUNCTION") && String.to_atom(System.get_env("CHECKSUM_FUNCTION")) + checksum_function: System.get_env("CHECKSUM_FUNCTION") && String.to_atom(System.get_env("CHECKSUM_FUNCTION")) config :explorer, Explorer.Chain.Cache.Blocks, - ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), - global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) + ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), + global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) config :explorer, Explorer.Chain.Cache.Transactions, - ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), - global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) + ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), + global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) + +config :explorer, Explorer.Chain.Cache.TransactionsApiV2, + ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), + global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) config :explorer, Explorer.Chain.Cache.Accounts, - ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), - global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) + ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), + global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) config :explorer, Explorer.Chain.Cache.Uncles, - ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), - global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) + ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false), + global_ttl: if(disable_indexer == "true", do: :timer.seconds(5)) config :explorer, Explorer.ThirdPartyIntegrations.Sourcify, - server_url: System.get_env("SOURCIFY_SERVER_URL") || "https://sourcify.dev/server", - enabled: System.get_env("ENABLE_SOURCIFY_INTEGRATION") == "true", - chain_id: System.get_env("CHAIN_ID"), - repo_url: System.get_env("SOURCIFY_REPO_URL") || "https://repo.sourcify.dev/contracts" + server_url: System.get_env("SOURCIFY_SERVER_URL") || "https://sourcify.dev/server", + enabled: System.get_env("ENABLE_SOURCIFY_INTEGRATION") == "true", + chain_id: System.get_env("CHAIN_ID"), + repo_url: System.get_env("SOURCIFY_REPO_URL") || "https://repo.sourcify.dev/contracts" config :explorer, Explorer.SmartContract.RustVerifierInterface, - service_url: System.get_env("RUST_VERIFICATION_SERVICE_URL"), - enabled: System.get_env("ENABLE_RUST_VERIFICATION_SERVICE") == "true" + service_url: System.get_env("RUST_VERIFICATION_SERVICE_URL"), + enabled: System.get_env("ENABLE_RUST_VERIFICATION_SERVICE") == "true" config :explorer, Explorer.ThirdPartyIntegrations.AirTable, - table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), - api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") + table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), + api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") config :explorer, Explorer.Mailer, - adapter: Bamboo.SendGridAdapter, - api_key: System.get_env("ACCOUNT_SENDGRID_API_KEY") + adapter: Bamboo.SendGridAdapter, + api_key: System.get_env("ACCOUNT_SENDGRID_API_KEY") config :explorer, Explorer.Account, - enabled: System.get_env("ACCOUNT_ENABLED") == "true", - sendgrid: [ - sender: System.get_env("ACCOUNT_SENDGRID_SENDER"), - template: System.get_env("ACCOUNT_SENDGRID_TEMPLATE") - ] + enabled: System.get_env("ACCOUNT_ENABLED") == "true", + sendgrid: [ + sender: System.get_env("ACCOUNT_SENDGRID_SENDER"), + template: System.get_env("ACCOUNT_SENDGRID_TEMPLATE") + ] {token_id_migration_first_block, _} = Integer.parse(System.get_env("TOKEN_ID_MIGRATION_FIRST_BLOCK", "0")) {token_id_migration_concurrency, _} = Integer.parse(System.get_env("TOKEN_ID_MIGRATION_CONCURRENCY", "1")) {token_id_migration_batch_size, _} = Integer.parse(System.get_env("TOKEN_ID_MIGRATION_BATCH_SIZE", "500")) config :explorer, :token_id_migration, - first_block: token_id_migration_first_block, - concurrency: token_id_migration_concurrency, - batch_size: token_id_migration_batch_size + first_block: token_id_migration_first_block, + concurrency: token_id_migration_concurrency, + batch_size: token_id_migration_batch_size ############### ### Indexer ### @@ -425,23 +437,30 @@ block_transformer = end config :indexer, - block_transformer: block_transformer, - ecto_repos: [Explorer.Repo.Local], - metadata_updater_seconds_interval: - String.to_integer(System.get_env("TOKEN_METADATA_UPDATE_INTERVAL") || "#{2 * 24 * 60 * 60}"), - health_check_port: port || 4001, - block_ranges: System.get_env("BLOCK_RANGES") || "", - first_block: System.get_env("FIRST_BLOCK") || "", - last_block: System.get_env("LAST_BLOCK") || "", - metrics_enabled: System.get_env("METRICS_ENABLED") || false, - trace_first_block: System.get_env("TRACE_FIRST_BLOCK") || "", - trace_last_block: System.get_env("TRACE_LAST_BLOCK") || "", - fetch_rewards_way: System.get_env("FETCH_REWARDS_WAY", "trace_block") + block_transformer: block_transformer, + ecto_repos: [Explorer.Repo.Local], + metadata_updater_seconds_interval: + String.to_integer(System.get_env("TOKEN_METADATA_UPDATE_INTERVAL") || "#{2 * 24 * 60 * 60}"), + health_check_port: port || 4001, + block_ranges: System.get_env("BLOCK_RANGES") || "", + first_block: System.get_env("FIRST_BLOCK") || "", + last_block: System.get_env("LAST_BLOCK") || "", + metrics_enabled: System.get_env("METRICS_ENABLED") || false, + trace_first_block: System.get_env("TRACE_FIRST_BLOCK") || "", + trace_last_block: System.get_env("TRACE_LAST_BLOCK") || "", + fetch_rewards_way: System.get_env("FETCH_REWARDS_WAY", "trace_block") + +{receipts_batch_size, _} = Integer.parse(System.get_env("INDEXER_RECEIPTS_BATCH_SIZE", "250")) +{receipts_concurrency, _} = Integer.parse(System.get_env("INDEXER_RECEIPTS_CONCURRENCY", "10")) + +config :indexer, + receipts_batch_size: receipts_batch_size, + receipts_concurrency: receipts_concurrency config :indexer, Indexer.Fetcher.PendingTransaction.Supervisor, - disabled?: - System.get_env("ETHEREUM_JSONRPC_VARIANT") == "besu" || - System.get_env("INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER", "false") == "true" + disabled?: + System.get_env("ETHEREUM_JSONRPC_VARIANT") == "besu" || + System.get_env("INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER", "false") == "true" token_balance_on_demand_fetcher_threshold_minutes = System.get_env("TOKEN_BALANCE_ON_DEMAND_FETCHER_THRESHOLD_MINUTES") @@ -466,26 +485,26 @@ coin_balance_on_demand_fetcher_threshold = config :indexer, Indexer.Fetcher.CoinBalanceOnDemand, threshold: coin_balance_on_demand_fetcher_threshold config :indexer, Indexer.Fetcher.BlockReward.Supervisor, - disabled?: System.get_env("INDEXER_DISABLE_BLOCK_REWARD_FETCHER", "false") == "true" + disabled?: System.get_env("INDEXER_DISABLE_BLOCK_REWARD_FETCHER", "false") == "true" config :indexer, Indexer.Fetcher.InternalTransaction.Supervisor, - disabled?: System.get_env("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER", "false") == "true" + disabled?: System.get_env("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER", "false") == "true" config :indexer, Indexer.Fetcher.CoinBalance.Supervisor, - disabled?: System.get_env("INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER", "false") == "true" + disabled?: System.get_env("INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER", "false") == "true" config :indexer, Indexer.Fetcher.TokenUpdater.Supervisor, - disabled?: System.get_env("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER", "false") == "true" + disabled?: System.get_env("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER", "false") == "true" config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer.Supervisor, - disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true" + disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true" config :indexer, Indexer.Supervisor, enabled: System.get_env("DISABLE_INDEXER") != "true" config :indexer, Indexer.Block.Realtime.Supervisor, enabled: System.get_env("DISABLE_REALTIME_INDEXER") != "true" config :indexer, Indexer.Fetcher.TokenInstance.Supervisor, - disabled?: System.get_env("DISABLE_TOKEN_INSTANCE_FETCHER", "false") == "true" + disabled?: System.get_env("DISABLE_TOKEN_INSTANCE_FETCHER", "false") == "true" blocks_catchup_fetcher_batch_size_default_str = "10" blocks_catchup_fetcher_concurrency_default_str = "10" @@ -497,12 +516,9 @@ blocks_catchup_fetcher_concurrency_default_str = "10" Integer.parse(System.get_env("INDEXER_CATCHUP_BLOCKS_CONCURRENCY", blocks_catchup_fetcher_concurrency_default_str)) config :indexer, Indexer.Block.Catchup.Fetcher, - batch_size: blocks_catchup_fetcher_batch_size, - concurrency: blocks_catchup_fetcher_concurrency + batch_size: blocks_catchup_fetcher_batch_size, + concurrency: blocks_catchup_fetcher_concurrency -if File.exists?("#{Path.absname(__DIR__)}/runtime/#{config_env()}.exs") do - Code.require_file("#{config_env()}.exs", "#{Path.absname(__DIR__)}/runtime") -end {internal_transaction_fetcher_batch_size, _} = Integer.parse(System.get_env("INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE", "8")) @@ -511,9 +527,20 @@ end Integer.parse(System.get_env("INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY", "8")) config :indexer, Indexer.Fetcher.InternalTransaction, - batch_size: internal_transaction_fetcher_batch_size, - concurrency: internal_transaction_fetcher_concurrency + batch_size: internal_transaction_fetcher_batch_size, + concurrency: internal_transaction_fetcher_concurrency +{coin_balance_fetcher_batch_size, _} = Integer.parse(System.get_env("INDEXER_COIN_BALANCES_BATCH_SIZE", "500")) + +{coin_balance_fetcher_concurrency, _} = Integer.parse(System.get_env("INDEXER_COIN_BALANCES_CONCURRENCY", "4")) + +config :indexer, Indexer.Fetcher.CoinBalance, + batch_size: coin_balance_fetcher_batch_size, + concurrency: coin_balance_fetcher_concurrency + +if File.exists?("#{Path.absname(__DIR__)}/runtime/#{config_env()}.exs") do + Code.require_file("#{config_env()}.exs", "#{Path.absname(__DIR__)}/runtime") +end for config <- "../apps/*/config/runtime/#{config_env()}.exs" |> Path.expand(__DIR__) |> Path.wildcard() do if File.exists?(config) do diff --git a/config/runtime/test.exs b/config/runtime/test.exs index f9bc04c0b75c..d020cf8fc807 100644 --- a/config/runtime/test.exs +++ b/config/runtime/test.exs @@ -7,6 +7,7 @@ alias EthereumJSONRPC.Variant ###################### config :block_scout_web, BlockScoutWeb.CsvExportController, itx_export_enabled: true +config :block_scout_web, BlockScoutWeb.API.V2, enabled: true ######################## ### Ethereum JSONRPC ### diff --git a/docker-compose/docker-compose-no-build-erigon.yml b/docker-compose/docker-compose-no-build-erigon.yml index 5f3eb2a0122e..b1640627a815 100644 --- a/docker-compose/docker-compose-no-build-erigon.yml +++ b/docker-compose/docker-compose-no-build-erigon.yml @@ -58,3 +58,13 @@ services: - ./envs/common-smart-contract-verifier.env ports: - 8043:8043 + + visualizer: + image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest} + restart: always + container_name: 'visualizer' + env_file: + - ./envs/common-visualizer.env + ports: + - 8050:8050 + diff --git a/docker-compose/docker-compose-no-build-ganache.yml b/docker-compose/docker-compose-no-build-ganache.yml index 22442c15a98c..177d402e47bf 100644 --- a/docker-compose/docker-compose-no-build-ganache.yml +++ b/docker-compose/docker-compose-no-build-ganache.yml @@ -60,3 +60,12 @@ services: ports: - 8043:8043 + visualizer: + image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest} + restart: always + container_name: 'visualizer' + env_file: + - ./envs/common-visualizer.env + ports: + - 8050:8050 + diff --git a/docker-compose/docker-compose-no-build-geth.yml b/docker-compose/docker-compose-no-build-geth.yml index 0fd7caa74db8..116ee4afe79a 100644 --- a/docker-compose/docker-compose-no-build-geth.yml +++ b/docker-compose/docker-compose-no-build-geth.yml @@ -58,3 +58,13 @@ services: - ./envs/common-smart-contract-verifier.env ports: - 8043:8043 + + visualizer: + image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest} + restart: always + container_name: 'visualizer' + env_file: + - ./envs/common-visualizer.env + ports: + - 8050:8050 + diff --git a/docker-compose/docker-compose-no-build-hardhat-network.yml b/docker-compose/docker-compose-no-build-hardhat-network.yml index 9fabf5f3e185..3748696d8353 100644 --- a/docker-compose/docker-compose-no-build-hardhat-network.yml +++ b/docker-compose/docker-compose-no-build-hardhat-network.yml @@ -57,3 +57,13 @@ services: - ./envs/common-smart-contract-verifier.env ports: - 8043:8043 + + visualizer: + image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest} + restart: always + container_name: 'visualizer' + env_file: + - ./envs/common-visualizer.env + ports: + - 8050:8050 + diff --git a/docker-compose/docker-compose-no-build-nethermind.yml b/docker-compose/docker-compose-no-build-nethermind.yml index e5d607428012..300d63c8278d 100644 --- a/docker-compose/docker-compose-no-build-nethermind.yml +++ b/docker-compose/docker-compose-no-build-nethermind.yml @@ -58,3 +58,13 @@ services: - ./envs/common-smart-contract-verifier.env ports: - 8043:8043 + + visualizer: + image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest} + restart: always + container_name: 'visualizer' + env_file: + - ./envs/common-visualizer.env + ports: + - 8050:8050 + diff --git a/docker-compose/docker-compose-no-build-no-db-container.yml b/docker-compose/docker-compose-no-build-no-db-container.yml index 24295de00e3a..c73468c81cd0 100644 --- a/docker-compose/docker-compose-no-build-no-db-container.yml +++ b/docker-compose/docker-compose-no-build-no-db-container.yml @@ -41,3 +41,13 @@ services: - ./envs/common-smart-contract-verifier.env ports: - 8043:8043 + + visualizer: + image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest} + restart: always + container_name: 'visualizer' + env_file: + - ./envs/common-visualizer.env + ports: + - 8050:8050 + diff --git a/docker-compose/docker-compose-no-rust-verification.yml b/docker-compose/docker-compose-no-rust-services.yml similarity index 97% rename from docker-compose/docker-compose-no-rust-verification.yml rename to docker-compose/docker-compose-no-rust-services.yml index 287912e609e0..f0ede3f16325 100644 --- a/docker-compose/docker-compose-no-rust-verification.yml +++ b/docker-compose/docker-compose-no-rust-services.yml @@ -52,6 +52,7 @@ services: - ./envs/common-blockscout.env environment: ENABLE_RUST_VERIFICATION_SERVICE: 'false' + VISUALIZE_SOL2UML_ENABLED: 'false' ports: - 4000:4000 volumes: diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 24b67343ac5c..63223524638b 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -64,3 +64,12 @@ services: - ./envs/common-smart-contract-verifier.env ports: - 8043:8043 + + visualizer: + image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest} + restart: always + container_name: 'visualizer' + env_file: + - ./envs/common-visualizer.env + ports: + - 8050:8050 diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index cffb1aac85e9..73c936dbfa47 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -86,6 +86,10 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false # INDEXER_CATCHUP_BLOCKS_CONCURRENCY= # INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE= # INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY= +# INDEXER_COIN_BALANCES_BATCH_SIZE= +# INDEXER_COIN_BALANCES_CONCURRENCY= +# INDEXER_RECEIPTS_BATCH_SIZE= +# INDEXER_RECEIPTS_CONCURRENCY= # TOKEN_ID_MIGRATION_FIRST_BLOCK= # TOKEN_ID_MIGRATION_CONCURRENCY= # TOKEN_ID_MIGRATION_BATCH_SIZE= @@ -133,6 +137,8 @@ API_RATE_LIMIT_STATIC_API_KEY= FETCH_REWARDS_WAY=trace_block ENABLE_RUST_VERIFICATION_SERVICE=true RUST_VERIFICATION_SERVICE_URL=http://host.docker.internal:8043/ +VISUALIZE_SOL2UML_ENABLED=true +VISUALIZE_SOL2UML_SERVICE_URL=http://host.docker.internal:8050/ # DATABASE_READ_ONLY_API_URL= # ACCOUNT_DATABASE_URL= # ACCOUNT_POOL_SIZE= diff --git a/docker-compose/envs/common-visualizer.env b/docker-compose/envs/common-visualizer.env new file mode 100644 index 000000000000..b4fd470849cb --- /dev/null +++ b/docker-compose/envs/common-visualizer.env @@ -0,0 +1 @@ +VISUALIZER__SERVER__GRPC__ENABLED=false diff --git a/docker/Makefile b/docker/Makefile index 29f285b08ad5..6423dd549b2e 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -467,6 +467,18 @@ endif ifdef INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY=$(INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY)' endif +ifdef INDEXER_COIN_BALANCES_BATCH_SIZE + BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_BATCH_SIZE=$(INDEXER_RECEIPTS_BATCH_SIZE)' +endif +ifdef INDEXER_COIN_BALANCES_CONCURRENCY + BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_CONCURRENCY=$(INDEXER_RECEIPTS_CONCURRENCY)' +endif +ifdef INDEXER_RECEIPTS_BATCH_SIZE + BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_BATCH_SIZE=$(INDEXER_RECEIPTS_BATCH_SIZE)' +endif +ifdef INDEXER_RECEIPTS_CONCURRENCY + BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_CONCURRENCY=$(INDEXER_RECEIPTS_CONCURRENCY)' +endif ifdef INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE=$(INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE)' endif @@ -545,6 +557,13 @@ endif ifdef ACCOUNT_CLOAK_KEY BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_CLOAK_KEY=$(ACCOUNT_CLOAK_KEY)' endif +ifdef VISUALIZE_SOL2UML_ENABLED + BLOCKSCOUT_CONTAINER_PARAMS += -e 'VISUALIZE_SOL2UML_ENABLED=$(VISUALIZE_SOL2UML_ENABLED)' +endif +ifdef VISUALIZE_SOL2UML_SERVICE_URL + BLOCKSCOUT_CONTAINER_PARAMS += -e 'VISUALIZE_SOL2UML_SERVICE_URL=$(VISUALIZE_SOL2UML_SERVICE_URL)' +endif + HAS_BLOCKSCOUT_IMAGE := $(shell docker images | grep -sw "${BS_CONTAINER_IMAGE} ") build: diff --git a/mix.lock b/mix.lock index d6db8c60311d..e556d348228c 100644 --- a/mix.lock +++ b/mix.lock @@ -36,8 +36,8 @@ "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "digital_token": {:hex, :digital_token, "0.4.0", "2ad6894d4a40be8b2890aad286ecd5745fa473fa5699d80361a8c94428edcd1f", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a178edf61d1fee5bb3c34e14b0f4ee21809ee87cade8738f87337e59e5e66e26"}, "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, - "ecto": {:hex, :ecto, "3.9.1", "67173b1687afeb68ce805ee7420b4261649d5e2deed8fe5550df23bab0bc4396", [: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", "c80bb3d736648df790f7f92f81b36c922d9dd3203ca65be4ff01d067f54eb304"}, - "ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.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", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"}, + "ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [: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", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"}, + "ecto_sql": {:hex, :ecto_sql, "3.9.1", "9bd5894eecc53d5b39d0c95180d4466aff00e10679e13a5cfa725f6f85c03c22", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.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", "5fd470a4fff2e829bbf9dcceb7f3f9f6d1e49b4241e802f614de6b8b67c51118"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, "elixir_talk": {:hex, :elixir_talk, "1.2.0", "f246f401ee3188f0aa5500a1b7cc2aadb0b7075e709bcfcf922ca1e0f517258f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.4.0", [hex: :yamerl, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 1.0.0", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "3aa1e22c7f159cb7bf0727b1ab9d070f4348a824a19ac360fca139b5ef38646b"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, @@ -47,7 +47,7 @@ "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.10.0", "4d4c9877da2d0417fd832907d69974e8328969f75fafc79b05ccf85f549f6281", [: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", "adc040cde7b97f7fd7c0b35dd69ddb6fcf607303ae6355bb1851deae1f8b0652"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.28.0", "506f5d36a2b72a21bbcb6e55dfdc5c3ff7f1c07d65e516461125158d10661beb", [: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.34", [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", "83342ff668aedf3aa5c54b048ce1da0f91317b6596b14880a5f87d45cd1c49d2"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.15.0", "3a834dfaf4daa0723cac165d528eacdbc3f9daec580f817b2847007fe07afdca", [:mix], [{: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.28", [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]}, {:ratio, "~> 2.4", [hex: :ratio, repo: "hexpm", optional: false]}], "hexpm", "bac7c3f6042482869dd67445adddaec2c263f561a8c2035eac7bd5f9d5ae1691"}, - "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [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", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"}, + "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [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", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, "ex_json_schema": {:hex, :ex_json_schema, "0.9.2", "c9a42e04e70cd70eb11a8903a22e8ec344df16edef4cb8e6ec84ed0caffc9f0f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4854329cb352b6c01c4c4b8dbfb3be14dc5bea19ea13e0eafade4ff22ba55224"}, "ex_keccak": {:hex, :ex_keccak, "0.6.0", "0e1f8974dd6630dd4fb0b64f9eabbceeffb9675da3ab95dea653798365802cf4", [:mix], [{:rustler, "~> 0.26", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "84b20cfe6a063edab311b2c8ff8b221698c84cbd5fbdba059e51636540142538"}, "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"}, @@ -125,7 +125,7 @@ "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"}, "recon": {:hex, :recon, "2.5.2", "cba53fa8db83ad968c9a652e09c3ed7ddcc4da434f27c3eaa9ca47ffb2b1ff03", [:mix, :rebar3], [], "hexpm", "2c7523c8dee91dff41f6b3d63cba2bd49eb6d2fe5bf1eec0df7f87eb5e230e1c"}, "redix": {:hex, :redix, "1.2.0", "0d7eb3ccb7b82c461a6ea28b65c2c04486093d816dd6d901a09164800e004df1", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e1e0deb14599da07c77e66956a12863e85ee270ada826804a0ba8e61657e22a3"}, - "remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"}, + "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": {:hex, :rustler, "0.26.0", "06a2773d453ee3e9109efda643cf2ae633dedea709e2455ac42b83637c9249bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "42961e9d2083d004d5a53e111ad1f0c347efd9a05cb2eb2ffa1d037cdc74db91"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, "spandex": {:hex, :spandex, "3.2.0", "f8cd40146ea988c87f3c14054150c9a47ba17e53cd4515c00e1f93c29c45404d", [:mix], [{:decorator, "~> 1.2", [hex: :decorator, repo: "hexpm", optional: true]}, {:optimal, "~> 0.3.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d0a7d5aef4c5af9cf5467f2003e8a5d8d2bdae3823a6cc95d776b9a2251d4d03"},