Merge branch 'release/v2021.09.01-otterscan'

This commit is contained in:
Willian Mitsuda 2021-09-05 00:22:15 -03:00
commit 7c5be049b1
31 changed files with 776 additions and 272 deletions

2
4bytes

@ -1 +1 @@
Subproject commit 79965318da56eed67366cf399ee5661b51af49cb Subproject commit 1cc7e25c840ae9d985c12768b0cbd0ece3fc5400

View File

@ -13,7 +13,7 @@ The entire build process will take place inside the docker multi-stage build.
Clone Otterscan repo and its submodules. Checkout the tag corresponding to your Erigon + Otterscan patches. It uses the same version tag from Erigon + Otterscan repo, i.e., if you built the `v2021.07.01-otterscan`, you should build the `v2021.07.01-otterscan` of Otterscan. Clone Otterscan repo and its submodules. Checkout the tag corresponding to your Erigon + Otterscan patches. It uses the same version tag from Erigon + Otterscan repo, i.e., if you built the `v2021.07.01-otterscan`, you should build the `v2021.07.01-otterscan` of Otterscan.
``` ```
git clone --recurse-submodules git@github.com:wmitsuda/otterscan.git git clone --recurse-submodules https://github.com/wmitsuda/otterscan.git
cd otterscan cd otterscan
git checkout <version-tag-otterscan> git checkout <version-tag-otterscan>
DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile . DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile .
@ -63,4 +63,4 @@ npm install
npm start npm start
``` ```
Otterscan should now be running at `http://localhost:3000/`. Otterscan should now be running at `http://localhost:3000/`.

199
package-lock.json generated
View File

@ -5,12 +5,14 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "otterscan",
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@blackbox-vision/react-qr-reader": "^5.0.0",
"@chainlink/contracts": "^0.2.1", "@chainlink/contracts": "^0.2.1",
"@craco/craco": "^6.2.0", "@craco/craco": "^6.2.0",
"@fontsource/fira-code": "^4.5.0", "@fontsource/fira-code": "^4.5.1",
"@fontsource/roboto": "^4.5.0", "@fontsource/roboto": "^4.5.0",
"@fontsource/roboto-mono": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0",
"@fontsource/space-grotesk": "^4.5.0", "@fontsource/space-grotesk": "^4.5.0",
@ -19,17 +21,17 @@
"@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15", "@fortawesome/react-fontawesome": "^0.1.15",
"@headlessui/react": "^1.4.0", "@headlessui/react": "^1.4.1",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^14.17.5", "@types/node": "^14.17.5",
"@types/react": "^17.0.17", "@types/react": "^17.0.19",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8", "@types/react-router-dom": "^5.1.8",
"chart.js": "^3.5.0", "chart.js": "^3.5.1",
"ethers": "^5.4.1", "ethers": "^5.4.1",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
@ -38,10 +40,10 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3", "react-error-boundary": "^3.1.3",
"react-image": "^4.0.3", "react-image": "^4.0.3",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"serve": "^12.0.0", "serve": "^12.0.0",
"typescript": "^4.3.5", "typescript": "^4.4.2",
"use-keyboard-shortcut": "^1.0.6", "use-keyboard-shortcut": "^1.0.6",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
@ -1208,6 +1210,19 @@
"version": "0.2.3", "version": "0.2.3",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@blackbox-vision/react-qr-reader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@blackbox-vision/react-qr-reader/-/react-qr-reader-5.0.0.tgz",
"integrity": "sha512-VLNKwwJTv4UX1inUNgt2aGC2yIhKBYptW9EOhn7Nq//WzjD5KvHG7WR48HTzGUZ2s/EA0XlxZSfKOarHV1Vb/A==",
"dependencies": {
"@zxing/browser": "0.0.7",
"@zxing/library": "^0.18.3"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
}
},
"node_modules/@chainlink/contracts": { "node_modules/@chainlink/contracts": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz",
@ -1993,9 +2008,9 @@
} }
}, },
"node_modules/@fontsource/fira-code": { "node_modules/@fontsource/fira-code": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.1.tgz",
"integrity": "sha512-fxRV3qt0eJaIXZvICXZMhXVR0lSyxZTC0cnM+1Ma/1JShGrIjCQ3yJ0W05rwaEoF3cAbpU2lKMrXfE7Of/zpIA==" "integrity": "sha512-8KTCsfs5m3UgICpHLglIKAS7vc2FFOu7/vvpWcE/42SWbh+9X8EJbEyJp6W96kU5iDVlAlUv4Cqc36Z9XUpLmA=="
}, },
"node_modules/@fontsource/roboto": { "node_modules/@fontsource/roboto": {
"version": "4.5.0", "version": "4.5.0",
@ -2111,9 +2126,9 @@
} }
}, },
"node_modules/@headlessui/react": { "node_modules/@headlessui/react": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
"integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==", "integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@ -3063,9 +3078,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "17.0.17", "version": "17.0.19",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.17.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz",
"integrity": "sha512-nrfi7I13cAmrd0wje8czYpf5SFbryczCtPzFc6ijqvdjKcyA3tCvGxwchOUlxb2ucBPuJ9Y3oUqKrRqZvrz0lw==", "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -3511,6 +3526,37 @@
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz", "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz",
"integrity": "sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg==" "integrity": "sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg=="
}, },
"node_modules/@zxing/browser": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz",
"integrity": "sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==",
"optionalDependencies": {
"@zxing/text-encoding": "^0.9.0"
},
"peerDependencies": {
"@zxing/library": "^0.18.3"
}
},
"node_modules/@zxing/library": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz",
"integrity": "sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==",
"dependencies": {
"ts-custom-error": "^3.0.0"
},
"engines": {
"node": ">= 10.4.0"
},
"optionalDependencies": {
"@zxing/text-encoding": "~0.9.0"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"node_modules/abab": { "node_modules/abab": {
"version": "2.0.5", "version": "2.0.5",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
@ -5530,9 +5576,9 @@
} }
}, },
"node_modules/chart.js": { "node_modules/chart.js": {
"version": "3.5.0", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA==" "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ=="
}, },
"node_modules/check-types": { "node_modules/check-types": {
"version": "11.1.2", "version": "11.1.2",
@ -14395,11 +14441,11 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "5.2.0", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0", "hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
@ -14415,15 +14461,15 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "5.2.0", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react-router": "5.2.0", "react-router": "5.2.1",
"tiny-invariant": "^1.0.2", "tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0" "tiny-warning": "^1.0.0"
}, },
@ -17824,6 +17870,14 @@
"version": "1.0.1", "version": "1.0.1",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ts-custom-error": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz",
"integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ts-pnp": { "node_modules/ts-pnp": {
"version": "1.2.0", "version": "1.2.0",
"license": "MIT", "license": "MIT",
@ -17936,9 +17990,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.3.5", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -20239,6 +20293,15 @@
"@bcoe/v8-coverage": { "@bcoe/v8-coverage": {
"version": "0.2.3" "version": "0.2.3"
}, },
"@blackbox-vision/react-qr-reader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@blackbox-vision/react-qr-reader/-/react-qr-reader-5.0.0.tgz",
"integrity": "sha512-VLNKwwJTv4UX1inUNgt2aGC2yIhKBYptW9EOhn7Nq//WzjD5KvHG7WR48HTzGUZ2s/EA0XlxZSfKOarHV1Vb/A==",
"requires": {
"@zxing/browser": "0.0.7",
"@zxing/library": "^0.18.3"
}
},
"@chainlink/contracts": { "@chainlink/contracts": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz",
@ -20678,9 +20741,9 @@
} }
}, },
"@fontsource/fira-code": { "@fontsource/fira-code": {
"version": "4.5.0", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.1.tgz",
"integrity": "sha512-fxRV3qt0eJaIXZvICXZMhXVR0lSyxZTC0cnM+1Ma/1JShGrIjCQ3yJ0W05rwaEoF3cAbpU2lKMrXfE7Of/zpIA==" "integrity": "sha512-8KTCsfs5m3UgICpHLglIKAS7vc2FFOu7/vvpWcE/42SWbh+9X8EJbEyJp6W96kU5iDVlAlUv4Cqc36Z9XUpLmA=="
}, },
"@fontsource/roboto": { "@fontsource/roboto": {
"version": "4.5.0", "version": "4.5.0",
@ -20767,9 +20830,9 @@
} }
}, },
"@headlessui/react": { "@headlessui/react": {
"version": "1.4.0", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
"integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==", "integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
"requires": {} "requires": {}
}, },
"@istanbuljs/load-nyc-config": { "@istanbuljs/load-nyc-config": {
@ -21377,9 +21440,9 @@
"version": "1.5.4" "version": "1.5.4"
}, },
"@types/react": { "@types/react": {
"version": "17.0.17", "version": "17.0.19",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.17.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz",
"integrity": "sha512-nrfi7I13cAmrd0wje8czYpf5SFbryczCtPzFc6ijqvdjKcyA3tCvGxwchOUlxb2ucBPuJ9Y3oUqKrRqZvrz0lw==", "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -21699,6 +21762,29 @@
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz", "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz",
"integrity": "sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg==" "integrity": "sha512-uUrgZ8AxS+Lio0fZKAipJjAh415JyrOZowliZAzmnJSsf7piVL5w+G0+gFJ0KSu3QRhvui/7zuvpLz03YjXAhg=="
}, },
"@zxing/browser": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.0.7.tgz",
"integrity": "sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==",
"requires": {
"@zxing/text-encoding": "^0.9.0"
}
},
"@zxing/library": {
"version": "0.18.6",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.18.6.tgz",
"integrity": "sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==",
"requires": {
"@zxing/text-encoding": "~0.9.0",
"ts-custom-error": "^3.0.0"
}
},
"@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"abab": { "abab": {
"version": "2.0.5" "version": "2.0.5"
}, },
@ -23101,9 +23187,9 @@
"version": "1.0.2" "version": "1.0.2"
}, },
"chart.js": { "chart.js": {
"version": "3.5.0", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA==" "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ=="
}, },
"check-types": { "check-types": {
"version": "11.1.2" "version": "11.1.2"
@ -28992,11 +29078,11 @@
"version": "0.8.3" "version": "0.8.3"
}, },
"react-router": { "react-router": {
"version": "5.2.0", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==", "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0", "hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
@ -29029,15 +29115,15 @@
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "5.2.0", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==", "integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==",
"requires": { "requires": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.12.13",
"history": "^4.9.0", "history": "^4.9.0",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react-router": "5.2.0", "react-router": "5.2.1",
"tiny-invariant": "^1.0.2", "tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0" "tiny-warning": "^1.0.0"
} }
@ -31386,6 +31472,11 @@
"tryer": { "tryer": {
"version": "1.0.1" "version": "1.0.1"
}, },
"ts-custom-error": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz",
"integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A=="
},
"ts-pnp": { "ts-pnp": {
"version": "1.2.0" "version": "1.2.0"
}, },
@ -31456,9 +31547,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "4.3.5", "version": "4.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==" "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ=="
}, },
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4" "version": "1.0.4"

View File

@ -4,9 +4,10 @@
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@blackbox-vision/react-qr-reader": "^5.0.0",
"@chainlink/contracts": "^0.2.1", "@chainlink/contracts": "^0.2.1",
"@craco/craco": "^6.2.0", "@craco/craco": "^6.2.0",
"@fontsource/fira-code": "^4.5.0", "@fontsource/fira-code": "^4.5.1",
"@fontsource/roboto": "^4.5.0", "@fontsource/roboto": "^4.5.0",
"@fontsource/roboto-mono": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0",
"@fontsource/space-grotesk": "^4.5.0", "@fontsource/space-grotesk": "^4.5.0",
@ -15,17 +16,17 @@
"@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15", "@fortawesome/react-fontawesome": "^0.1.15",
"@headlessui/react": "^1.4.0", "@headlessui/react": "^1.4.1",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^14.17.5", "@types/node": "^14.17.5",
"@types/react": "^17.0.17", "@types/react": "^17.0.19",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8", "@types/react-router-dom": "^5.1.8",
"chart.js": "^3.5.0", "chart.js": "^3.5.1",
"ethers": "^5.4.1", "ethers": "^5.4.1",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"react": "^17.0.2", "react": "^17.0.2",
@ -34,10 +35,10 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3", "react-error-boundary": "^3.1.3",
"react-image": "^4.0.3", "react-image": "^4.0.3",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"serve": "^12.0.0", "serve": "^12.0.0",
"typescript": "^4.3.5", "typescript": "^4.4.2",
"use-keyboard-shortcut": "^1.0.6", "use-keyboard-shortcut": "^1.0.6",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },

View File

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
PARAMS="{\"erigonURL\": $(echo $ERIGON_URL | jq -aR .)}" PARAMS="{\"erigonURL\": $(echo $ERIGON_URL | jq -aR .), \"assetsURLPrefix\": \"\"}"
echo $PARAMS > /usr/share/nginx/html/config.json echo $PARAMS > /usr/share/nginx/html/config.json
nginx -g "daemon off;" nginx -g "daemon off;"

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useMemo, useContext } from "react"; import React, { useState, useEffect, useMemo, useContext } from "react";
import { useParams, useLocation, useHistory } from "react-router-dom"; import { useParams, useLocation, useHistory } from "react-router-dom";
import { BlockTag } from "@ethersproject/abstract-provider";
import { getAddress, isAddress } from "@ethersproject/address"; import { getAddress, isAddress } from "@ethersproject/address";
import queryString from "query-string"; import queryString from "query-string";
import Blockies from "react-blockies"; import Blockies from "react-blockies";
@ -16,6 +17,7 @@ import { RuntimeContext } from "./useRuntime";
import { useENSCache } from "./useReverseCache"; import { useENSCache } from "./useReverseCache";
import { useFeeToggler } from "./search/useFeeToggler"; import { useFeeToggler } from "./search/useFeeToggler";
import { SelectionContext, useSelection } from "./useSelection"; import { SelectionContext, useSelection } from "./useSelection";
import { useMultipleETHUSDOracle } from "./usePriceOracle";
type BlockParams = { type BlockParams = {
addressOrName: string; addressOrName: string;
@ -150,6 +152,14 @@ const AddressTransactions: React.FC = () => {
const page = useMemo(() => controller?.getPage(), [controller]); const page = useMemo(() => controller?.getPage(), [controller]);
const reverseCache = useENSCache(provider, page); const reverseCache = useENSCache(provider, page);
const blockTags: BlockTag[] = useMemo(() => {
if (!page) {
return [];
}
return page.map((p) => p.blockNumber);
}, [page]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
document.title = `Address ${params.addressOrName} | Otterscan`; document.title = `Address ${params.addressOrName} | Otterscan`;
const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const [feeDisplay, feeDisplayToggler] = useFeeToggler();
@ -215,6 +225,7 @@ const AddressTransactions: React.FC = () => {
ensCache={reverseCache} ensCache={reverseCache}
selectedAddress={checksummedAddress} selectedAddress={checksummedAddress}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap}
/> />
))} ))}
<div className="flex justify-between items-baseline py-3"> <div className="flex justify-between items-baseline py-3">

View File

@ -17,11 +17,14 @@ import BlockLink from "./components/BlockLink";
import DecoratedAddressLink from "./components/DecoratedAddressLink"; import DecoratedAddressLink from "./components/DecoratedAddressLink";
import TransactionValue from "./components/TransactionValue"; import TransactionValue from "./components/TransactionValue";
import FormattedBalance from "./components/FormattedBalance"; import FormattedBalance from "./components/FormattedBalance";
import ETH2USDValue from "./components/ETH2USDValue";
import USDValue from "./components/USDValue";
import HexValue from "./components/HexValue"; import HexValue from "./components/HexValue";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useLatestBlockNumber } from "./useLatestBlock"; import { useLatestBlockNumber } from "./useLatestBlock";
import { blockTxsURL } from "./url"; import { blockTxsURL } from "./url";
import { useBlockData } from "./useErigonHooks"; import { useBlockData } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle";
type BlockParams = { type BlockParams = {
blockNumberOrHash: string; blockNumberOrHash: string;
@ -48,11 +51,12 @@ const Block: React.FC = () => {
}, [block]); }, [block]);
const burntFees = const burntFees =
block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed); block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed);
const netFeeReward = block && block.feeReward.sub(burntFees ?? 0); const netFeeReward = block?.feeReward ?? BigNumber.from(0);
const gasUsedPerc = const gasUsedPerc =
block && block.gasUsed.mul(10000).div(block.gasLimit).toNumber() / 100; block && block.gasUsed.mul(10000).div(block.gasLimit).toNumber() / 100;
const latestBlockNumber = useLatestBlockNumber(provider); const latestBlockNumber = useLatestBlockNumber(provider);
const blockETHUSDPrice = useETHUSDOracle(provider, block?.number);
return ( return (
<StandardFrame> <StandardFrame>
@ -91,18 +95,23 @@ const Block: React.FC = () => {
<DecoratedAddressLink address={block.miner} miner /> <DecoratedAddressLink address={block.miner} miner />
</InfoRow> </InfoRow>
<InfoRow title="Block Reward"> <InfoRow title="Block Reward">
<TransactionValue <TransactionValue value={block.blockReward.add(netFeeReward)} />
value={block.blockReward.add(netFeeReward ?? 0)} {!netFeeReward.isZero() && (
/>
{!block.feeReward.isZero() && (
<> <>
{" "} {" "}
(<TransactionValue value={block.blockReward} hideUnit /> +{" "} (<TransactionValue value={block.blockReward} hideUnit /> +{" "}
<TransactionValue <TransactionValue value={netFeeReward} hideUnit />)
value={netFeeReward ?? BigNumber.from(0)} </>
hideUnit )}
/> {blockETHUSDPrice && (
) <>
{" "}
<span className="px-2 border-yellow-200 border rounded-lg bg-yellow-100 text-yellow-600">
<ETH2USDValue
ethAmount={block.blockReward.add(netFeeReward)}
eth2USDValue={blockETHUSDPrice}
/>
</span>
</> </>
)} )}
</InfoRow> </InfoRow>
@ -153,7 +162,9 @@ const Block: React.FC = () => {
{extraStr} (Hex:{" "} {extraStr} (Hex:{" "}
<span className="font-data">{block.extraData}</span>) <span className="font-data">{block.extraData}</span>)
</InfoRow> </InfoRow>
<InfoRow title="Ether Price">N/A</InfoRow> <InfoRow title="Ether Price">
<USDValue value={blockETHUSDPrice} />
</InfoRow>
<InfoRow title="Difficult">{commify(block.difficulty)}</InfoRow> <InfoRow title="Difficult">{commify(block.difficulty)}</InfoRow>
<InfoRow title="Total Difficult"> <InfoRow title="Total Difficult">
{commify(block.totalDifficulty.toString())} {commify(block.totalDifficulty.toString())}

View File

@ -47,6 +47,7 @@ const BlockTransactions: React.FC = () => {
<StandardFrame> <StandardFrame>
<BlockTransactionHeader blockTag={blockNumber.toNumber()} /> <BlockTransactionHeader blockTag={blockNumber.toNumber()} />
<BlockTransactionResults <BlockTransactionResults
blockTag={blockNumber.toNumber()}
page={txs} page={txs}
total={totalTxs ?? 0} total={totalTxs ?? 0}
pageNumber={pageNumber} pageNumber={pageNumber}

View File

@ -3,7 +3,9 @@ import { NavLink, useHistory } from "react-router-dom";
import { commify } from "@ethersproject/units"; import { commify } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn"; import { faBurn } from "@fortawesome/free-solid-svg-icons/faBurn";
import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode";
import Logo from "./Logo"; import Logo from "./Logo";
import CameraScanner from "./search/CameraScanner";
import Timestamp from "./components/Timestamp"; import Timestamp from "./components/Timestamp";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useLatestBlock } from "./useLatestBlock"; import { useLatestBlock } from "./useLatestBlock";
@ -30,11 +32,13 @@ const Home: React.FC = () => {
}; };
const latestBlock = useLatestBlock(provider); const latestBlock = useLatestBlock(provider);
const [isScanning, setScanning] = useState<boolean>(false);
document.title = "Home | Otterscan"; document.title = "Home | Otterscan";
return ( return (
<div className="m-auto"> <div className="m-auto">
{isScanning && <CameraScanner turnOffScan={() => setScanning(false)} />}
<Logo /> <Logo />
<form <form
className="flex flex-col" className="flex flex-col"
@ -42,16 +46,26 @@ const Home: React.FC = () => {
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
> >
<input <div className="flex mb-10">
className="w-full border rounded focus:outline-none px-2 py-1 mb-10" <input
type="text" className="w-full border-l border-t border-b rounded-l focus:outline-none px-2 py-1"
size={50} type="text"
placeholder="Search by address / txn hash / block number / ENS name" size={50}
onChange={handleChange} placeholder="Search by address / txn hash / block number / ENS name"
autoFocus onChange={handleChange}
></input> autoFocus
/>
<button
className="border rounded-r bg-skin-button-fill hover:bg-skin-button-hover-fill focus:outline-none px-2 py-1 text-base text-skin-button flex justify-center items-center"
type="button"
onClick={() => setScanning(true)}
title="Scan an ETH address using your camera"
>
<FontAwesomeIcon icon={faQrcode} />
</button>
</div>
<button <button
className="mx-auto px-3 py-1 mb-10 rounded bg-gray-100 hover:bg-gray-200 focus:outline-none" className="mx-auto px-3 py-1 mb-10 rounded bg-skin-button-fill hover:bg-skin-button-hover-fill focus:outline-none"
type="submit" type="submit"
> >
Search Search

View File

@ -1,7 +1,10 @@
import React, { useState, useRef, useContext } from "react"; import React, { useState, useRef, useContext } from "react";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQrcode } from "@fortawesome/free-solid-svg-icons/faQrcode";
import useKeyboardShortcut from "use-keyboard-shortcut"; import useKeyboardShortcut from "use-keyboard-shortcut";
import PriceBox from "./PriceBox"; import PriceBox from "./PriceBox";
import CameraScanner from "./search/CameraScanner";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
const Title: React.FC = () => { const Title: React.FC = () => {
@ -29,46 +32,59 @@ const Title: React.FC = () => {
searchRef.current?.focus(); searchRef.current?.focus();
}); });
const [isScanning, setScanning] = useState<boolean>(false);
return ( return (
<div className="px-9 py-2 flex justify-between items-baseline"> <>
<Link className="self-center" to="/"> {isScanning && <CameraScanner turnOffScan={() => setScanning(false)} />}
<div className="text-2xl text-link-blue font-title font-bold flex items-center space-x-2"> <div className="px-9 py-2 flex justify-between items-baseline">
<img <Link className="self-center" to="/">
className="rounded-full" <div className="text-2xl text-link-blue font-title font-bold flex items-center space-x-2">
src="/otter.jpg" <img
width={32} className="rounded-full"
height={32} src="/otter.jpg"
alt="An otter scanning" width={32}
title="An otter scanning" height={32}
/> alt="An otter scanning"
<span>Otterscan</span> title="An otter scanning"
</div> />
</Link> <span>Otterscan</span>
<div className="flex items-baseline space-x-3"> </div>
{provider?.network.chainId === 1 && <PriceBox />} </Link>
<form <div className="flex items-baseline space-x-3">
className="flex" {provider?.network.chainId === 1 && <PriceBox />}
onSubmit={handleSubmit} <form
autoComplete="off" className="flex"
spellCheck={false} onSubmit={handleSubmit}
> autoComplete="off"
<input spellCheck={false}
className="w-full border-t border-b border-l rounded-l focus:outline-none px-2 py-1 text-sm"
type="text"
size={60}
placeholder='Type "/" to search by address / txn hash / block number / ENS name'
onChange={handleChange}
ref={searchRef}
/>
<button
className="rounded-r border-t border-b border-r bg-gray-100 hover:bg-gray-200 focus:outline-none px-2 py-1 text-sm text-gray-500"
type="submit"
> >
Search <input
</button> className="w-full border-t border-b border-l rounded-l focus:outline-none px-2 py-1 text-sm"
</form> type="text"
size={60}
placeholder='Type "/" to search by address / txn hash / block number / ENS name'
onChange={handleChange}
ref={searchRef}
/>
<button
className="border bg-skin-button-fill hover:bg-skin-button-hover-fill focus:outline-none px-2 py-1 text-sm text-skin-button"
type="button"
onClick={() => setScanning(true)}
title="Scan an ETH address using your camera"
>
<FontAwesomeIcon icon={faQrcode} />
</button>
<button
className="rounded-r border-t border-b border-r bg-skin-button-fill hover:bg-skin-button-hover-fill focus:outline-none px-2 py-1 text-sm text-skin-button"
type="submit"
>
Search
</button>
</form>
</div>
</div> </div>
</div> </>
); );
}; };

View File

@ -2,12 +2,14 @@ import React, { useMemo, useContext } from "react";
import { Route, Switch, useParams } from "react-router-dom"; import { Route, Switch, useParams } from "react-router-dom";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle"; import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import Tab from "./components/Tab"; import Tab from "./components/Tab";
import Details from "./transaction/Details"; import Details from "./transaction/Details";
import Logs from "./transaction/Logs"; import Logs from "./transaction/Logs";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { SelectionContext, useSelection } from "./useSelection"; import { SelectionContext, useSelection } from "./useSelection";
import { useInternalOperations, useTxData } from "./useErigonHooks"; import { useInternalOperations, useTxData } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle";
type TransactionParams = { type TransactionParams = {
txhash: string; txhash: string;
@ -27,7 +29,7 @@ const Transaction: React.FC = () => {
} }
for (const t of internalOps) { for (const t of internalOps) {
if (t.to === txData.miner) { if (t.to === txData.confirmedData?.miner) {
return true; return true;
} }
} }
@ -36,16 +38,30 @@ const Transaction: React.FC = () => {
const selectionCtx = useSelection(); const selectionCtx = useSelection();
const blockETHUSDPrice = useETHUSDOracle(
provider,
txData?.confirmedData?.blockNumber
);
return ( return (
<StandardFrame> <StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle> <StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txhash}</span> not found.
</div>
</ContentFrame>
)}
{txData && ( {txData && (
<SelectionContext.Provider value={selectionCtx}> <SelectionContext.Provider value={selectionCtx}>
<div className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white"> <div className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<Tab href={`/tx/${txhash}`}>Overview</Tab> <Tab href={`/tx/${txhash}`}>Overview</Tab>
<Tab href={`/tx/${txhash}/logs`}> {txData.confirmedData?.blockNumber !== undefined && (
Logs{txData && ` (${txData.logs.length})`} <Tab href={`/tx/${txhash}/logs`}>
</Tab> Logs{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</Tab>
)}
</div> </div>
<Switch> <Switch>
<Route path="/tx/:txhash/" exact> <Route path="/tx/:txhash/" exact>
@ -53,6 +69,7 @@ const Transaction: React.FC = () => {
txData={txData} txData={txData}
internalOps={internalOps} internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner} sendsEthToMiner={sendsEthToMiner}
ethUSDPrice={blockETHUSDPrice}
/> />
</Route> </Route>
<Route path="/tx/:txhash/logs/" exact> <Route path="/tx/:txhash/logs/" exact>

View File

@ -1,4 +1,5 @@
import React, { useContext } from "react"; import React, { useContext, useMemo } from "react";
import { BlockTag } from "@ethersproject/abstract-provider";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import PageControl from "../search/PageControl"; import PageControl from "../search/PageControl";
import ResultHeader from "../search/ResultHeader"; import ResultHeader from "../search/ResultHeader";
@ -10,14 +11,17 @@ import { SelectionContext, useSelection } from "../useSelection";
import { useENSCache } from "../useReverseCache"; import { useENSCache } from "../useReverseCache";
import { ProcessedTransaction } from "../types"; import { ProcessedTransaction } from "../types";
import { PAGE_SIZE } from "../params"; import { PAGE_SIZE } from "../params";
import { useMultipleETHUSDOracle } from "../usePriceOracle";
type BlockTransactionResultsProps = { type BlockTransactionResultsProps = {
blockTag: BlockTag;
page?: ProcessedTransaction[]; page?: ProcessedTransaction[];
total: number; total: number;
pageNumber: number; pageNumber: number;
}; };
const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
blockTag,
page, page,
total, total,
pageNumber, pageNumber,
@ -26,6 +30,8 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const [feeDisplay, feeDisplayToggler] = useFeeToggler();
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const reverseCache = useENSCache(provider, page); const reverseCache = useENSCache(provider, page);
const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
return ( return (
<ContentFrame> <ContentFrame>
@ -55,6 +61,7 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
tx={tx} tx={tx}
ensCache={reverseCache} ensCache={reverseCache}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap}
/> />
))} ))}
<div className="flex justify-between items-baseline py-3"> <div className="flex justify-between items-baseline py-3">

View File

@ -41,11 +41,13 @@ const DecoratedAddresssLink: React.FC<DecoratedAddressLinkProps> = ({
return ( return (
<div <div
className={`flex items-baseline space-x-1 ${txFrom ? "bg-red-50" : ""} ${ className={`flex items-baseline space-x-1 ${
txTo ? "bg-green-50" : "" txFrom ? "bg-skin-from" : ""
} ${mint ? "italic text-green-500 hover:text-green-700" : ""} ${ } ${txTo ? "bg-skin-to" : ""} ${
burn ? "line-through text-orange-500 hover:text-orange-700" : "" mint ? "italic text-green-500 hover:text-green-700" : ""
} ${selfDestruct ? "line-through opacity-70 hover:opacity-100" : ""}`} } ${burn ? "line-through text-orange-500 hover:text-orange-700" : ""} ${
selfDestruct ? "line-through opacity-70 hover:opacity-100" : ""
}`}
> >
{creation && ( {creation && (
<span className="text-yellow-300" title="Contract creation"> <span className="text-yellow-300" title="Contract creation">

View File

@ -0,0 +1,26 @@
import React from "react";
import { BigNumber, FixedNumber } from "@ethersproject/bignumber";
import { commify } from "@ethersproject/units";
type ETH2USDValueProps = {
ethAmount: BigNumber;
eth2USDValue: BigNumber;
};
const ETH2USDValue: React.FC<ETH2USDValueProps> = ({
ethAmount,
eth2USDValue,
}) => {
const value = ethAmount.mul(eth2USDValue).div(10 ** 8);
return (
<span className="text-xs">
$
<span className="font-balance">
{commify(FixedNumber.fromValue(value, 18).round(2).toString())}
</span>
</span>
);
};
export default React.memo(ETH2USDValue);

View File

@ -22,7 +22,9 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const network = provider?.network; const network = provider?.network;
const toMiner = txData.miner !== undefined && internalOp.to === txData.miner; const toMiner =
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
return ( return (
<> <>

View File

@ -16,8 +16,11 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
internalOp, internalOp,
}) => { }) => {
const fromMiner = const fromMiner =
txData.miner !== undefined && internalOp.from === txData.miner; txData.confirmedData?.miner !== undefined &&
const toMiner = txData.miner !== undefined && internalOp.to === txData.miner; internalOp.from === txData.confirmedData.miner;
const toMiner =
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
return ( return (
<div className="flex items-baseline space-x-1 text-xs"> <div className="flex items-baseline space-x-1 text-xs">

View File

@ -0,0 +1,29 @@
import React from "react";
import { BigNumber, FixedNumber } from "@ethersproject/bignumber";
import { commify } from "@ethersproject/units";
const ETH_FEED_DECIMALS = 8;
type USDValueProps = {
value: BigNumber | undefined;
};
const USDValue: React.FC<USDValueProps> = ({ value }) => (
<span className="text-sm">
{value ? (
<>
$
<span className="font-balance">
{commify(
FixedNumber.fromValue(value, ETH_FEED_DECIMALS).round(2).toString()
)}
</span>{" "}
<span className="text-xs text-gray-500">/ ETH</span>
</>
) : (
"N/A"
)}
</span>
);
export default React.memo(USDValue);

View File

@ -1,3 +1,30 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
:root {
--color-button-fill: 244, 244, 245; /* gray-100 */
--color-button-hover-fill: 228, 228, 231; /* gray-200 */
--color-button-text: 113, 113, 122; /* gray-500 */
--color-from-border: 254, 226, 226;
--color-from-text: 220, 38, 38;
--color-from-fill: 254, 242, 242;
--color-to-fill: 236, 253, 245;
--color-table-row-hover: 243, 244, 246;
}
.test-theme {
--color-button-fill: 14, 165, 233; /* sky-500 */
--color-button-hover-fill: 56, 189, 248; /* sky-400 */
--color-button-text: 186, 230, 253; /* sky-200 */
--color-from-border: 251, 146, 60;
--color-from-text: 249, 115, 22;
--color-from-fill: 254, 215, 170;
--color-to-fill: 125, 211, 252;
--color-table-row-hover: 2, 132, 199;
}
}

View File

@ -0,0 +1,54 @@
import React from "react";
import { useHistory } from "react-router-dom";
import { isAddress } from "@ethersproject/address";
import { QrReader } from "@blackbox-vision/react-qr-reader";
import { OnResultFunction } from "@blackbox-vision/react-qr-reader/dist-types/types";
import { BarcodeFormat } from "@zxing/library";
import { Dialog } from "@headlessui/react";
type CameraScannerProps = {
turnOffScan: () => void;
};
const CameraScanner: React.FC<CameraScannerProps> = ({ turnOffScan }) => {
const history = useHistory();
const evaluateScan: OnResultFunction = (result, error, codeReader) => {
console.log("scan");
if (!error && result?.getBarcodeFormat() === BarcodeFormat.QR_CODE) {
const text = result.getText();
console.log(`Scanned: ${text}`);
if (!isAddress(text)) {
console.warn("Not an ETH address");
return;
}
history.push(`/search?q=${text}`);
turnOffScan();
}
};
return (
<Dialog
className="fixed z-10 inset-0 overflow-y-auto"
open={true}
onClose={turnOffScan}
>
<div className="flex items-center justify-center min-h-screen">
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
<Dialog.Title className="absolute top-0 w-full text-center bg-white text-lg">
Point an ETH address QR code to camera
</Dialog.Title>
<div className="absolute inset-0 bg-transparent rounded min-w-max max-w-3xl w-full h-screen max-h-screen m-auto">
<QrReader
className="m-auto"
constraints={{}}
onResult={evaluateScan}
/>
</div>
</div>
</Dialog>
);
};
export default CameraScanner;

View File

@ -23,7 +23,9 @@ const ResultHeader: React.FC<ResultHeaderProps> = ({
className="text-link-blue hover:text-link-blue-hover" className="text-link-blue hover:text-link-blue-hover"
onClick={feeDisplayToggler} onClick={feeDisplayToggler}
> >
{feeDisplay === FeeDisplay.TX_FEE ? "Txn Fee" : "Gas Price"} {feeDisplay === FeeDisplay.TX_FEE && "Txn Fee"}
{feeDisplay === FeeDisplay.TX_FEE_USD && "Txn Fee (USD)"}
{feeDisplay === FeeDisplay.GAS_PRICE && "Gas Price"}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,4 +1,6 @@
import React from "react"; import React from "react";
import { BlockTag } from "@ethersproject/abstract-provider";
import { BigNumber } from "@ethersproject/bignumber";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle"; import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
@ -15,12 +17,14 @@ import TransactionValue from "../components/TransactionValue";
import { ENSReverseCache, ProcessedTransaction } from "../types"; import { ENSReverseCache, ProcessedTransaction } from "../types";
import { FeeDisplay } from "./useFeeToggler"; import { FeeDisplay } from "./useFeeToggler";
import { formatValue } from "../components/formatter"; import { formatValue } from "../components/formatter";
import ETH2USDValue from "../components/ETH2USDValue";
type TransactionItemProps = { type TransactionItemProps = {
tx: ProcessedTransaction; tx: ProcessedTransaction;
ensCache?: ENSReverseCache; ensCache?: ENSReverseCache;
selectedAddress?: string; selectedAddress?: string;
feeDisplay: FeeDisplay; feeDisplay: FeeDisplay;
priceMap: Record<BlockTag, BigNumber>;
}; };
const TransactionItem: React.FC<TransactionItemProps> = ({ const TransactionItem: React.FC<TransactionItemProps> = ({
@ -28,6 +32,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
ensCache, ensCache,
selectedAddress, selectedAddress,
feeDisplay, feeDisplay,
priceMap,
}) => { }) => {
let direction: Direction | undefined; let direction: Direction | undefined;
if (selectedAddress) { if (selectedAddress) {
@ -56,7 +61,9 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
return ( return (
<div <div
className={`grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 ${ className={`grid grid-cols-12 gap-x-1 items-baseline text-sm border-t border-gray-200 ${
flash ? "bg-yellow-100 hover:bg-yellow-200" : "hover:bg-gray-100" flash
? "bg-yellow-100 hover:bg-yellow-200"
: "hover:bg-skin-table-hover"
} px-2 py-3`} } px-2 py-3`}
> >
<div className="col-span-2 flex space-x-1 items-baseline"> <div className="col-span-2 flex space-x-1 items-baseline">
@ -121,9 +128,17 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<TransactionValue value={tx.value} /> <TransactionValue value={tx.value} />
</span> </span>
<span className="font-balance text-xs text-gray-500 truncate"> <span className="font-balance text-xs text-gray-500 truncate">
{feeDisplay === FeeDisplay.TX_FEE {feeDisplay === FeeDisplay.TX_FEE && formatValue(tx.fee, 18)}
? formatValue(tx.fee, 18) {feeDisplay === FeeDisplay.TX_FEE_USD &&
: formatValue(tx.gasPrice, 9)} (priceMap[tx.blockNumber] ? (
<ETH2USDValue
ethAmount={tx.fee}
eth2USDValue={priceMap[tx.blockNumber]}
/>
) : (
"N/A"
))}
{feeDisplay === FeeDisplay.GAS_PRICE && formatValue(tx.gasPrice, 9)}
</span> </span>
</div> </div>
); );

View File

@ -2,16 +2,16 @@ import { useState } from "react";
export enum FeeDisplay { export enum FeeDisplay {
TX_FEE, TX_FEE,
TX_FEE_USD,
GAS_PRICE, GAS_PRICE,
} }
export const useFeeToggler = (): [FeeDisplay, () => void] => { export const useFeeToggler = (): [FeeDisplay, () => void] => {
const [feeDisplay, setFeeDisplay] = useState<FeeDisplay>(FeeDisplay.TX_FEE); const [feeDisplay, setFeeDisplay] = useState<FeeDisplay>(FeeDisplay.TX_FEE);
const feeDisplayToggler = () => { const feeDisplayToggler = () => {
if (feeDisplay === FeeDisplay.TX_FEE) { setFeeDisplay(feeDisplay + 1);
setFeeDisplay(FeeDisplay.GAS_PRICE); if (feeDisplay === FeeDisplay.GAS_PRICE) {
} else { setFeeDisplay(0);
setFeeDisplay(FeeDisplay.TX_FEE);
} }
}; };

View File

@ -22,7 +22,7 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block, baseFeeDelta }) => {
const totalReward = block.blockReward.add(netFeeReward ?? 0); const totalReward = block.blockReward.add(netFeeReward ?? 0);
return ( return (
<div className="grid grid-cols-9 gap-x-2 px-3 py-2 hover:bg-gray-100"> <div className="grid grid-cols-9 gap-x-2 px-3 py-2 hover:bg-skin-table-hover">
<div> <div>
<BlockLink blockTag={block.number} /> <BlockLink blockTag={block.number} />
</div> </div>

View File

@ -1,5 +1,5 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { formatEther } from "@ethersproject/units"; import { BigNumber } from "@ethersproject/bignumber";
import { toUtf8String } from "@ethersproject/strings"; import { toUtf8String } from "@ethersproject/strings";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
@ -19,7 +19,9 @@ import MethodName from "../components/MethodName";
import TransactionType from "../components/TransactionType"; import TransactionType from "../components/TransactionType";
import RewardSplit from "./RewardSplit"; import RewardSplit from "./RewardSplit";
import GasValue from "../components/GasValue"; import GasValue from "../components/GasValue";
import USDValue from "../components/USDValue";
import FormattedBalance from "../components/FormattedBalance"; import FormattedBalance from "../components/FormattedBalance";
import ETH2USDValue from "../components/ETH2USDValue";
import TokenTransferItem from "../TokenTransferItem"; import TokenTransferItem from "../TokenTransferItem";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
import PercentageBar from "../components/PercentageBar"; import PercentageBar from "../components/PercentageBar";
@ -31,16 +33,18 @@ type DetailsProps = {
txData: TransactionData; txData: TransactionData;
internalOps?: InternalOperation[]; internalOps?: InternalOperation[];
sendsEthToMiner: boolean; sendsEthToMiner: boolean;
ethUSDPrice: BigNumber | undefined;
}; };
const Details: React.FC<DetailsProps> = ({ const Details: React.FC<DetailsProps> = ({
txData, txData,
internalOps, internalOps,
sendsEthToMiner, sendsEthToMiner,
ethUSDPrice,
}) => { }) => {
const hasEIP1559 = const hasEIP1559 =
txData.blockBaseFeePerGas !== undefined && txData.confirmedData?.blockBaseFeePerGas !== undefined &&
txData.blockBaseFeePerGas !== null; txData.confirmedData?.blockBaseFeePerGas !== null;
const [inputMode, setInputMode] = useState<number>(0); const [inputMode, setInputMode] = useState<number>(0);
const utfInput = useMemo(() => { const utfInput = useMemo(() => {
@ -62,7 +66,9 @@ const Details: React.FC<DetailsProps> = ({
</div> </div>
</InfoRow> </InfoRow>
<InfoRow title="Status"> <InfoRow title="Status">
{txData.status ? ( {txData.confirmedData === undefined ? (
<span className="italic text-gray-400">Pending</span>
) : txData.confirmedData.status ? (
<span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-green-50 text-green-500 text-xs"> <span className="flex items-center w-min rounded-lg space-x-1 px-3 py-1 bg-green-50 text-green-500 text-xs">
<FontAwesomeIcon icon={faCheckCircle} size="1x" /> <FontAwesomeIcon icon={faCheckCircle} size="1x" />
<span>Success</span> <span>Success</span>
@ -74,38 +80,45 @@ const Details: React.FC<DetailsProps> = ({
</span> </span>
)} )}
</InfoRow> </InfoRow>
<InfoRow title="Block / Position"> {txData.confirmedData && (
<div className="flex items-baseline divide-x-2 divide-dotted divide-gray-300"> <>
<div className="flex space-x-1 items-baseline mr-3"> <InfoRow title="Block / Position">
<span className="text-orange-500"> <div className="flex items-baseline divide-x-2 divide-dotted divide-gray-300">
<FontAwesomeIcon icon={faCube} /> <div className="flex space-x-1 items-baseline mr-3">
</span> <span className="text-orange-500">
<BlockLink blockTag={txData.blockNumber} /> <FontAwesomeIcon icon={faCube} />
<BlockConfirmations confirmations={txData.confirmations} /> </span>
</div> <BlockLink blockTag={txData.confirmedData.blockNumber} />
<div className="flex space-x-2 items-baseline pl-3"> <BlockConfirmations
<RelativePosition confirmations={txData.confirmedData.confirmations}
pos={txData.transactionIndex} />
total={txData.blockTransactionCount - 1} </div>
/> <div className="flex space-x-2 items-baseline pl-3">
<PercentagePosition <RelativePosition
perc={ pos={txData.confirmedData.transactionIndex}
txData.transactionIndex / (txData.blockTransactionCount - 1) total={txData.confirmedData.blockTransactionCount - 1}
} />
/> <PercentagePosition
</div> perc={
</div> txData.confirmedData.transactionIndex /
</InfoRow> (txData.confirmedData.blockTransactionCount - 1)
<InfoRow title="Timestamp"> }
<Timestamp value={txData.timestamp} /> />
</InfoRow> </div>
</div>
</InfoRow>
<InfoRow title="Timestamp">
<Timestamp value={txData.confirmedData.timestamp} />
</InfoRow>
</>
)}
<InfoRow title="From / Nonce"> <InfoRow title="From / Nonce">
<div className="flex divide-x-2 divide-dotted divide-gray-300"> <div className="flex divide-x-2 divide-dotted divide-gray-300">
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={txData.from}> <AddressHighlighter address={txData.from}>
<DecoratedAddressLink <DecoratedAddressLink
address={txData.from} address={txData.from}
miner={txData.from === txData.miner} miner={txData.from === txData.confirmedData?.miner}
txFrom txFrom
/> />
</AddressHighlighter> </AddressHighlighter>
@ -122,22 +135,28 @@ const Details: React.FC<DetailsProps> = ({
<AddressHighlighter address={txData.to}> <AddressHighlighter address={txData.to}>
<DecoratedAddressLink <DecoratedAddressLink
address={txData.to} address={txData.to}
miner={txData.to === txData.miner} miner={txData.to === txData.confirmedData?.miner}
txTo txTo
/> />
</AddressHighlighter> </AddressHighlighter>
<Copy value={txData.to} /> <Copy value={txData.to} />
</div> </div>
) : txData.confirmedData === undefined ? (
<span className="italic text-gray-400">
Pending contract creation
</span>
) : ( ) : (
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<AddressHighlighter address={txData.createdContractAddress!}> <AddressHighlighter
address={txData.confirmedData?.createdContractAddress!}
>
<DecoratedAddressLink <DecoratedAddressLink
address={txData.createdContractAddress!} address={txData.confirmedData.createdContractAddress!}
creation creation
txTo txTo
/> />
</AddressHighlighter> </AddressHighlighter>
<Copy value={txData.createdContractAddress!} /> <Copy value={txData.confirmedData.createdContractAddress!} />
</div> </div>
)} )}
{internalOps && internalOps.length > 0 && ( {internalOps && internalOps.length > 0 && (
@ -170,9 +189,12 @@ const Details: React.FC<DetailsProps> = ({
</InfoRow> </InfoRow>
)} )}
<InfoRow title="Value"> <InfoRow title="Value">
<span className="rounded bg-gray-100 px-2 py-1 text-xs"> <FormattedBalance value={txData.value} /> Ether{" "}
{formatEther(txData.value)} Ether {!txData.value.isZero() && ethUSDPrice && (
</span> <span className="px-2 border-skin-from border rounded-lg bg-skin-from text-skin-from">
<ETH2USDValue ethAmount={txData.value} eth2USDValue={ethUSDPrice} />
</span>
)}
</InfoRow> </InfoRow>
<InfoRow <InfoRow
title={ title={
@ -211,58 +233,81 @@ const Details: React.FC<DetailsProps> = ({
</InfoRow> </InfoRow>
</> </>
)} )}
<InfoRow title="Gas Price"> {txData.gasPrice && (
<div className="flex items-baseline space-x-1"> <InfoRow title="Gas Price">
<span> <div className="flex items-baseline space-x-1">
<FormattedBalance value={txData.gasPrice} /> Ether ( <span>
<FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei) <FormattedBalance value={txData.gasPrice} /> Ether (
</span> <FormattedBalance value={txData.gasPrice} decimals={9} /> Gwei)
{sendsEthToMiner && (
<span className="rounded text-yellow-500 bg-yellow-100 text-xs px-2 py-1">
Flashbots
</span> </span>
)} {sendsEthToMiner && (
</div> <span className="rounded text-yellow-500 bg-yellow-100 text-xs px-2 py-1">
</InfoRow> Flashbots
<InfoRow title="Gas Used / Limit"> </span>
<div className="flex space-x-3 items-baseline"> )}
<div> </div>
<RelativePosition </InfoRow>
pos={<GasValue value={txData.gasUsed} />} )}
total={<GasValue value={txData.gasLimit} />} {txData.confirmedData && (
<InfoRow title="Gas Used / Limit">
<div className="flex space-x-3 items-baseline">
<div>
<RelativePosition
pos={<GasValue value={txData.confirmedData.gasUsed} />}
total={<GasValue value={txData.gasLimit} />}
/>
</div>
<PercentageBar
perc={
Math.round(
(txData.confirmedData.gasUsed.toNumber() /
txData.gasLimit.toNumber()) *
10000
) / 100
}
/> />
</div> </div>
<PercentageBar </InfoRow>
perc={ )}
Math.round( {txData.confirmedData && hasEIP1559 && (
(txData.gasUsed.toNumber() / txData.gasLimit.toNumber()) * 10000
) / 100
}
/>
</div>
</InfoRow>
{hasEIP1559 && (
<InfoRow title="Block Base Fee"> <InfoRow title="Block Base Fee">
<span> <span>
<FormattedBalance value={txData.blockBaseFeePerGas!} decimals={9} />{" "} <FormattedBalance
value={txData.confirmedData.blockBaseFeePerGas!}
decimals={9}
/>{" "}
Gwei ( Gwei (
<FormattedBalance <FormattedBalance
value={txData.blockBaseFeePerGas!} value={txData.confirmedData.blockBaseFeePerGas!}
decimals={0} decimals={0}
/>{" "} />{" "}
wei) wei)
</span> </span>
</InfoRow> </InfoRow>
)} )}
<InfoRow title="Transaction Fee"> {txData.confirmedData && (
<div className="space-y-3"> <>
<div> <InfoRow title="Transaction Fee">
<FormattedBalance value={txData.fee} /> Ether <div className="space-y-3">
</div> <div>
{hasEIP1559 && <RewardSplit txData={txData} />} <FormattedBalance value={txData.confirmedData.fee} /> Ether{" "}
</div> {ethUSDPrice && (
</InfoRow> <span className="px-2 border-skin-from border rounded-lg bg-skin-from text-skin-from">
<InfoRow title="Ether Price">N/A</InfoRow> <ETH2USDValue
ethAmount={txData.confirmedData.fee}
eth2USDValue={ethUSDPrice}
/>
</span>
)}
</div>
{hasEIP1559 && <RewardSplit txData={txData} />}
</div>
</InfoRow>
<InfoRow title="Ether Price">
<USDValue value={ethUSDPrice} />
</InfoRow>
</>
)}
<InfoRow title="Input Data"> <InfoRow title="Input Data">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex space-x-1"> <div className="flex space-x-1">

View File

@ -10,8 +10,8 @@ type LogsProps = {
const Logs: React.FC<LogsProps> = ({ txData }) => ( const Logs: React.FC<LogsProps> = ({ txData }) => (
<ContentFrame tabs> <ContentFrame tabs>
<div className="text-sm py-4">Transaction Receipt Event Logs</div> <div className="text-sm py-4">Transaction Receipt Event Logs</div>
{txData && {txData.confirmedData &&
txData.logs.map((l, i) => ( txData.confirmedData.logs.map((l, i) => (
<div className="flex space-x-10 py-5" key={i}> <div className="flex space-x-10 py-5" key={i}>
<div> <div>
<span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500"> <span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500">
@ -24,7 +24,7 @@ const Logs: React.FC<LogsProps> = ({ txData }) => (
<div className="col-span-11 mr-auto"> <div className="col-span-11 mr-auto">
<DecoratedAddressLink <DecoratedAddressLink
address={l.address} address={l.address}
miner={l.address === txData.miner} miner={l.address === txData.confirmedData?.miner}
txFrom={l.address === txData.from} txFrom={l.address === txData.from}
txTo={l.address === txData.to} txTo={l.address === txData.to}
/> />

View File

@ -11,8 +11,10 @@ type RewardSplitProps = {
}; };
const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => { const RewardSplit: React.FC<RewardSplitProps> = ({ txData }) => {
const paidFees = txData.gasPrice.mul(txData.gasUsed); const paidFees = txData.gasPrice.mul(txData.confirmedData!.gasUsed);
const burntFees = txData.blockBaseFeePerGas!.mul(txData.gasUsed); const burntFees = txData.confirmedData!.blockBaseFeePerGas!.mul(
txData.confirmedData!.gasUsed
);
const minerReward = paidFees.sub(burntFees); const minerReward = paidFees.sub(burntFees);
const burntPerc = const burntPerc =

View File

@ -38,29 +38,33 @@ export type ENSReverseCache = {
export type TransactionData = { export type TransactionData = {
transactionHash: string; transactionHash: string;
status: boolean;
blockNumber: number;
transactionIndex: number;
blockTransactionCount: number;
confirmations: number;
timestamp: number;
miner?: string;
from: string; from: string;
to: string; to?: string;
createdContractAddress?: string;
value: BigNumber; value: BigNumber;
tokenTransfers: TokenTransfer[]; tokenTransfers: TokenTransfer[];
tokenMetas: TokenMetas; tokenMetas: TokenMetas;
type: number; type: number;
maxFeePerGas?: BigNumber | undefined; maxFeePerGas?: BigNumber | undefined;
maxPriorityFeePerGas?: BigNumber | undefined; maxPriorityFeePerGas?: BigNumber | undefined;
fee: BigNumber;
blockBaseFeePerGas?: BigNumber | undefined | null;
gasPrice: BigNumber; gasPrice: BigNumber;
gasUsed: BigNumber;
gasLimit: BigNumber; gasLimit: BigNumber;
nonce: number; nonce: number;
data: string; data: string;
confirmedData?: ConfirmedTransactionData | undefined;
};
export type ConfirmedTransactionData = {
status: boolean;
blockNumber: number;
transactionIndex: number;
blockBaseFeePerGas?: BigNumber | undefined | null;
blockTransactionCount: number;
confirmations: number;
timestamp: number;
miner: string;
createdContractAddress?: string;
fee: BigNumber;
gasUsed: BigNumber;
logs: Log[]; logs: Log[];
}; };

View File

@ -48,12 +48,11 @@ export const readBlock = async (
const _rawBlock = await blockPromise; const _rawBlock = await blockPromise;
const _block = provider.formatter.block(_rawBlock.block); const _block = provider.formatter.block(_rawBlock.block);
const _rawIssuance = _rawBlock.issuance; const _rawIssuance = _rawBlock.issuance;
const fees = provider.formatter.bigNumber(_rawBlock.totalFees);
const extBlock: ExtendedBlock = { const extBlock: ExtendedBlock = {
blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward ?? 0), blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward ?? 0),
unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward ?? 0), unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward ?? 0),
feeReward: fees, feeReward: provider.formatter.bigNumber(_rawBlock.totalFees),
size: provider.formatter.number(_rawBlock.block.size), size: provider.formatter.number(_rawBlock.block.size),
sha3Uncles: _rawBlock.block.sha3Uncles, sha3Uncles: _rawBlock.block.sha3Uncles,
stateRoot: _rawBlock.block.stateRoot, stateRoot: _rawBlock.block.stateRoot,
@ -175,8 +174,8 @@ export const useBlockData = (
export const useTxData = ( export const useTxData = (
provider: JsonRpcProvider | undefined, provider: JsonRpcProvider | undefined,
txhash: string txhash: string
): TransactionData | undefined => { ): TransactionData | undefined | null => {
const [txData, setTxData] = useState<TransactionData>(); const [txData, setTxData] = useState<TransactionData | undefined | null>();
useEffect(() => { useEffect(() => {
if (!provider) { if (!provider) {
@ -188,24 +187,35 @@ export const useTxData = (
provider.getTransaction(txhash), provider.getTransaction(txhash),
provider.getTransactionReceipt(txhash), provider.getTransactionReceipt(txhash),
]); ]);
const _block = await readBlock(provider, _receipt.blockNumber.toString()); if (_response === null) {
setTxData(null);
return;
}
let _block: ExtendedBlock | undefined;
if (_response.blockNumber) {
_block = await readBlock(provider, _response.blockNumber.toString());
}
document.title = `Transaction ${_response.hash} | Otterscan`; document.title = `Transaction ${_response.hash} | Otterscan`;
// Extract token transfers // Extract token transfers
const tokenTransfers: TokenTransfer[] = []; const tokenTransfers: TokenTransfer[] = [];
for (const l of _receipt.logs) { if (_receipt) {
if (l.topics.length !== 3) { for (const l of _receipt.logs) {
continue; if (l.topics.length !== 3) {
continue;
}
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)),
to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)),
value: BigNumber.from(l.data),
});
} }
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)),
to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)),
value: BigNumber.from(l.data),
});
} }
// Extract token meta // Extract token meta
@ -228,31 +238,36 @@ export const useTxData = (
} }
setTxData({ setTxData({
transactionHash: _receipt.transactionHash, transactionHash: _response.hash,
status: _receipt.status === 1, from: _response.from,
blockNumber: _receipt.blockNumber, to: _response.to,
transactionIndex: _receipt.transactionIndex,
blockTransactionCount: _block.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block.timestamp,
miner: _block.miner,
from: _receipt.from,
to: _receipt.to,
createdContractAddress: _receipt.contractAddress,
value: _response.value, value: _response.value,
tokenTransfers, tokenTransfers,
tokenMetas, tokenMetas,
type: _response.type ?? 0, type: _response.type ?? 0,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
blockBaseFeePerGas: _block.baseFeePerGas,
maxFeePerGas: _response.maxFeePerGas, maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas, maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!, gasPrice: _response.gasPrice!,
gasUsed: _receipt.gasUsed,
gasLimit: _response.gasLimit, gasLimit: _response.gasLimit,
nonce: _response.nonce, nonce: _response.nonce,
data: _response.data, data: _response.data,
logs: _receipt.logs, confirmedData:
_receipt === null
? undefined
: {
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
blockBaseFeePerGas: _block!.baseFeePerGas,
blockTransactionCount: _block!.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block!.timestamp,
miner: _block!.miner,
createdContractAddress: _receipt.contractAddress,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
gasUsed: _receipt.gasUsed,
logs: _receipt.logs,
},
}); });
}; };
readTxData(); readTxData();
@ -263,13 +278,13 @@ export const useTxData = (
export const useInternalOperations = ( export const useInternalOperations = (
provider: JsonRpcProvider | undefined, provider: JsonRpcProvider | undefined,
txData: TransactionData | undefined txData: TransactionData | undefined | null
): InternalOperation[] | undefined => { ): InternalOperation[] | undefined => {
const [intTransfers, setIntTransfers] = useState<InternalOperation[]>(); const [intTransfers, setIntTransfers] = useState<InternalOperation[]>();
useEffect(() => { useEffect(() => {
const traceTransfers = async () => { const traceTransfers = async () => {
if (!provider || !txData) { if (!provider || !txData || !txData.confirmedData) {
return; return;
} }

78
src/usePriceOracle.ts Normal file
View File

@ -0,0 +1,78 @@
import { useEffect, useMemo, useState } from "react";
import { JsonRpcProvider, BlockTag } from "@ethersproject/providers";
import { Contract } from "@ethersproject/contracts";
import { BigNumber } from "@ethersproject/bignumber";
import AggregatorV3Interface from "@chainlink/contracts/abi/v0.8/AggregatorV3Interface.json";
export const useETHUSDOracle = (
provider: JsonRpcProvider | undefined,
blockTag: BlockTag | undefined
) => {
const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
if (blockTag === undefined) {
return undefined;
}
return priceMap[blockTag];
};
export const useMultipleETHUSDOracle = (
provider: JsonRpcProvider | undefined,
blockTags: (BlockTag | undefined)[]
) => {
const ethFeed = useMemo(() => {
if (!provider || provider.network.chainId !== 1) {
return undefined;
}
try {
return new Contract("eth-usd.data.eth", AggregatorV3Interface, provider);
} catch (err) {
console.error(err);
return undefined;
}
}, [provider]);
const [latestPriceData, setLatestPriceData] = useState<
Record<BlockTag, BigNumber>
>({});
useEffect(() => {
if (!ethFeed) {
return;
}
const priceReaders: Promise<BigNumber | undefined>[] = [];
for (const blockTag of blockTags) {
priceReaders.push(
(async () => {
try {
const priceData = await ethFeed.latestRoundData({ blockTag });
return BigNumber.from(priceData.answer);
} catch (err) {
console.error(err);
return undefined;
}
})()
);
}
const readData = async () => {
const results = await Promise.all(priceReaders);
const priceMap: Record<BlockTag, BigNumber> = {};
for (let i = 0; i < blockTags.length; i++) {
const blockTag = blockTags[i];
const result = results[i];
if (blockTag === undefined || result === undefined) {
continue;
}
priceMap[blockTag] = result;
}
setLatestPriceData(priceMap);
};
readData();
}, [ethFeed, blockTags]);
return latestPriceData;
};

View File

@ -1,5 +1,14 @@
const colors = require("tailwindcss/colors"); const colors = require("tailwindcss/colors");
function withOpacity(variableName) {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(var(${variableName}), ${opacityValue})`;
}
return `rgb(var(${variableName}))`;
};
}
module.exports = { module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
darkMode: false, // or 'media' or 'class' darkMode: false, // or 'media' or 'class'
@ -19,6 +28,28 @@ module.exports = {
balance: ["Fira Code"], balance: ["Fira Code"],
blocknum: ["Roboto"], blocknum: ["Roboto"],
}, },
borderColor: {
skin: {
from: withOpacity("--color-from-border"),
},
},
textColor: {
skin: {
button: withOpacity("--color-button-text"),
from: withOpacity("--color-from-text"),
},
},
backgroundColor: {
skin: {
"button-fill": withOpacity("--color-button-fill"),
"button-hover-fill": withOpacity("--color-button-hover-fill"),
from: withOpacity("--color-from-fill"),
to: withOpacity("--color-to-fill"),
"table-hover": withOpacity("--color-table-row-hover"),
},
},
}, },
}, },
variants: { variants: {

@ -1 +1 @@
Subproject commit 9bc40f37d95234810bc7e176513c8366c81080ce Subproject commit 7bfa06acc125a4874d86bc1fa8e4547a46846e31