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.
```
git clone --recurse-submodules git@github.com:wmitsuda/otterscan.git
git clone --recurse-submodules https://github.com/wmitsuda/otterscan.git
cd otterscan
git checkout <version-tag-otterscan>
DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile .
@ -63,4 +63,4 @@ npm install
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,
"packages": {
"": {
"name": "otterscan",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@blackbox-vision/react-qr-reader": "^5.0.0",
"@chainlink/contracts": "^0.2.1",
"@craco/craco": "^6.2.0",
"@fontsource/fira-code": "^4.5.0",
"@fontsource/fira-code": "^4.5.1",
"@fontsource/roboto": "^4.5.0",
"@fontsource/roboto-mono": "^4.5.0",
"@fontsource/space-grotesk": "^4.5.0",
@ -19,17 +21,17 @@
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@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/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24",
"@types/node": "^14.17.5",
"@types/react": "^17.0.17",
"@types/react": "^17.0.19",
"@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
"chart.js": "^3.5.0",
"chart.js": "^3.5.1",
"ethers": "^5.4.1",
"query-string": "^7.0.1",
"react": "^17.0.2",
@ -38,10 +40,10 @@
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.3",
"react-image": "^4.0.3",
"react-router-dom": "^5.2.0",
"react-router-dom": "^5.2.1",
"react-scripts": "4.0.3",
"serve": "^12.0.0",
"typescript": "^4.3.5",
"typescript": "^4.4.2",
"use-keyboard-shortcut": "^1.0.6",
"web-vitals": "^1.0.1"
},
@ -1208,6 +1210,19 @@
"version": "0.2.3",
"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": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz",
@ -1993,9 +2008,9 @@
}
},
"node_modules/@fontsource/fira-code": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.0.tgz",
"integrity": "sha512-fxRV3qt0eJaIXZvICXZMhXVR0lSyxZTC0cnM+1Ma/1JShGrIjCQ3yJ0W05rwaEoF3cAbpU2lKMrXfE7Of/zpIA=="
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.1.tgz",
"integrity": "sha512-8KTCsfs5m3UgICpHLglIKAS7vc2FFOu7/vvpWcE/42SWbh+9X8EJbEyJp6W96kU5iDVlAlUv4Cqc36Z9XUpLmA=="
},
"node_modules/@fontsource/roboto": {
"version": "4.5.0",
@ -2111,9 +2126,9 @@
}
},
"node_modules/@headlessui/react": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz",
"integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
"integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
"engines": {
"node": ">=10"
},
@ -3063,9 +3078,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "17.0.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.17.tgz",
"integrity": "sha512-nrfi7I13cAmrd0wje8czYpf5SFbryczCtPzFc6ijqvdjKcyA3tCvGxwchOUlxb2ucBPuJ9Y3oUqKrRqZvrz0lw==",
"version": "17.0.19",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz",
"integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -3511,6 +3526,37 @@
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz",
"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": {
"version": "2.0.5",
"license": "BSD-3-Clause"
@ -5530,9 +5576,9 @@
}
},
"node_modules/chart.js": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz",
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA=="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
"integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ=="
},
"node_modules/check-types": {
"version": "11.1.2",
@ -14395,11 +14441,11 @@
}
},
"node_modules/react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
@ -14415,15 +14461,15 @@
}
},
"node_modules/react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz",
"integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
@ -17824,6 +17870,14 @@
"version": "1.0.1",
"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": {
"version": "1.2.0",
"license": "MIT",
@ -17936,9 +17990,9 @@
}
},
"node_modules/typescript": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -20239,6 +20293,15 @@
"@bcoe/v8-coverage": {
"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": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@chainlink/contracts/-/contracts-0.2.1.tgz",
@ -20678,9 +20741,9 @@
}
},
"@fontsource/fira-code": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.0.tgz",
"integrity": "sha512-fxRV3qt0eJaIXZvICXZMhXVR0lSyxZTC0cnM+1Ma/1JShGrIjCQ3yJ0W05rwaEoF3cAbpU2lKMrXfE7Of/zpIA=="
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.5.1.tgz",
"integrity": "sha512-8KTCsfs5m3UgICpHLglIKAS7vc2FFOu7/vvpWcE/42SWbh+9X8EJbEyJp6W96kU5iDVlAlUv4Cqc36Z9XUpLmA=="
},
"@fontsource/roboto": {
"version": "4.5.0",
@ -20767,9 +20830,9 @@
}
},
"@headlessui/react": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.0.tgz",
"integrity": "sha512-C+FmBVF6YGvqcEI5fa2dfVbEaXr2RGR6Kw1E5HXIISIZEfsrH/yuCgsjWw5nlRF9vbCxmQ/EKs64GAdKeb8gCw==",
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
"integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
"requires": {}
},
"@istanbuljs/load-nyc-config": {
@ -21377,9 +21440,9 @@
"version": "1.5.4"
},
"@types/react": {
"version": "17.0.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.17.tgz",
"integrity": "sha512-nrfi7I13cAmrd0wje8czYpf5SFbryczCtPzFc6ijqvdjKcyA3tCvGxwchOUlxb2ucBPuJ9Y3oUqKrRqZvrz0lw==",
"version": "17.0.19",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz",
"integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -21699,6 +21762,29 @@
"resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.6.0.tgz",
"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": {
"version": "2.0.5"
},
@ -23101,9 +23187,9 @@
"version": "1.0.2"
},
"chart.js": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz",
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA=="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz",
"integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ=="
},
"check-types": {
"version": "11.1.2"
@ -28992,11 +29078,11 @@
"version": "0.8.3"
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"requires": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
@ -29029,15 +29115,15 @@
}
},
"react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz",
"integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==",
"requires": {
"@babel/runtime": "^7.1.2",
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
@ -31386,6 +31472,11 @@
"tryer": {
"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": {
"version": "1.2.0"
},
@ -31456,9 +31547,9 @@
}
},
"typescript": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
"integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA=="
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz",
"integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ=="
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4"

View File

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

View File

@ -1,4 +1,4 @@
#!/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
nginx -g "daemon off;"

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect, useMemo, useContext } from "react";
import { useParams, useLocation, useHistory } from "react-router-dom";
import { BlockTag } from "@ethersproject/abstract-provider";
import { getAddress, isAddress } from "@ethersproject/address";
import queryString from "query-string";
import Blockies from "react-blockies";
@ -16,6 +17,7 @@ import { RuntimeContext } from "./useRuntime";
import { useENSCache } from "./useReverseCache";
import { useFeeToggler } from "./search/useFeeToggler";
import { SelectionContext, useSelection } from "./useSelection";
import { useMultipleETHUSDOracle } from "./usePriceOracle";
type BlockParams = {
addressOrName: string;
@ -150,6 +152,14 @@ const AddressTransactions: React.FC = () => {
const page = useMemo(() => controller?.getPage(), [controller]);
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`;
const [feeDisplay, feeDisplayToggler] = useFeeToggler();
@ -215,6 +225,7 @@ const AddressTransactions: React.FC = () => {
ensCache={reverseCache}
selectedAddress={checksummedAddress}
feeDisplay={feeDisplay}
priceMap={priceMap}
/>
))}
<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 TransactionValue from "./components/TransactionValue";
import FormattedBalance from "./components/FormattedBalance";
import ETH2USDValue from "./components/ETH2USDValue";
import USDValue from "./components/USDValue";
import HexValue from "./components/HexValue";
import { RuntimeContext } from "./useRuntime";
import { useLatestBlockNumber } from "./useLatestBlock";
import { blockTxsURL } from "./url";
import { useBlockData } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle";
type BlockParams = {
blockNumberOrHash: string;
@ -48,11 +51,12 @@ const Block: React.FC = () => {
}, [block]);
const burntFees =
block?.baseFeePerGas && block.baseFeePerGas.mul(block.gasUsed);
const netFeeReward = block && block.feeReward.sub(burntFees ?? 0);
const netFeeReward = block?.feeReward ?? BigNumber.from(0);
const gasUsedPerc =
block && block.gasUsed.mul(10000).div(block.gasLimit).toNumber() / 100;
const latestBlockNumber = useLatestBlockNumber(provider);
const blockETHUSDPrice = useETHUSDOracle(provider, block?.number);
return (
<StandardFrame>
@ -91,18 +95,23 @@ const Block: React.FC = () => {
<DecoratedAddressLink address={block.miner} miner />
</InfoRow>
<InfoRow title="Block Reward">
<TransactionValue
value={block.blockReward.add(netFeeReward ?? 0)}
/>
{!block.feeReward.isZero() && (
<TransactionValue value={block.blockReward.add(netFeeReward)} />
{!netFeeReward.isZero() && (
<>
{" "}
(<TransactionValue value={block.blockReward} hideUnit /> +{" "}
<TransactionValue
value={netFeeReward ?? BigNumber.from(0)}
hideUnit
/>
)
<TransactionValue value={netFeeReward} 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>
@ -153,7 +162,9 @@ const Block: React.FC = () => {
{extraStr} (Hex:{" "}
<span className="font-data">{block.extraData}</span>)
</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="Total Difficult">
{commify(block.totalDifficulty.toString())}

View File

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

View File

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

View File

@ -1,7 +1,10 @@
import React, { useState, useRef, useContext } from "react";
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 PriceBox from "./PriceBox";
import CameraScanner from "./search/CameraScanner";
import { RuntimeContext } from "./useRuntime";
const Title: React.FC = () => {
@ -29,46 +32,59 @@ const Title: React.FC = () => {
searchRef.current?.focus();
});
const [isScanning, setScanning] = useState<boolean>(false);
return (
<div className="px-9 py-2 flex justify-between items-baseline">
<Link className="self-center" to="/">
<div className="text-2xl text-link-blue font-title font-bold flex items-center space-x-2">
<img
className="rounded-full"
src="/otter.jpg"
width={32}
height={32}
alt="An otter scanning"
title="An otter scanning"
/>
<span>Otterscan</span>
</div>
</Link>
<div className="flex items-baseline space-x-3">
{provider?.network.chainId === 1 && <PriceBox />}
<form
className="flex"
onSubmit={handleSubmit}
autoComplete="off"
spellCheck={false}
>
<input
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"
<>
{isScanning && <CameraScanner turnOffScan={() => setScanning(false)} />}
<div className="px-9 py-2 flex justify-between items-baseline">
<Link className="self-center" to="/">
<div className="text-2xl text-link-blue font-title font-bold flex items-center space-x-2">
<img
className="rounded-full"
src="/otter.jpg"
width={32}
height={32}
alt="An otter scanning"
title="An otter scanning"
/>
<span>Otterscan</span>
</div>
</Link>
<div className="flex items-baseline space-x-3">
{provider?.network.chainId === 1 && <PriceBox />}
<form
className="flex"
onSubmit={handleSubmit}
autoComplete="off"
spellCheck={false}
>
Search
</button>
</form>
<input
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="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>
</>
);
};

View File

@ -2,12 +2,14 @@ import React, { useMemo, useContext } from "react";
import { Route, Switch, useParams } from "react-router-dom";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import Tab from "./components/Tab";
import Details from "./transaction/Details";
import Logs from "./transaction/Logs";
import { RuntimeContext } from "./useRuntime";
import { SelectionContext, useSelection } from "./useSelection";
import { useInternalOperations, useTxData } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle";
type TransactionParams = {
txhash: string;
@ -27,7 +29,7 @@ const Transaction: React.FC = () => {
}
for (const t of internalOps) {
if (t.to === txData.miner) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
@ -36,16 +38,30 @@ const Transaction: React.FC = () => {
const selectionCtx = useSelection();
const blockETHUSDPrice = useETHUSDOracle(
provider,
txData?.confirmedData?.blockNumber
);
return (
<StandardFrame>
<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 && (
<SelectionContext.Provider value={selectionCtx}>
<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}/logs`}>
Logs{txData && ` (${txData.logs.length})`}
</Tab>
{txData.confirmedData?.blockNumber !== undefined && (
<Tab href={`/tx/${txhash}/logs`}>
Logs{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</Tab>
)}
</div>
<Switch>
<Route path="/tx/:txhash/" exact>
@ -53,6 +69,7 @@ const Transaction: React.FC = () => {
txData={txData}
internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner}
ethUSDPrice={blockETHUSDPrice}
/>
</Route>
<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 PageControl from "../search/PageControl";
import ResultHeader from "../search/ResultHeader";
@ -10,14 +11,17 @@ import { SelectionContext, useSelection } from "../useSelection";
import { useENSCache } from "../useReverseCache";
import { ProcessedTransaction } from "../types";
import { PAGE_SIZE } from "../params";
import { useMultipleETHUSDOracle } from "../usePriceOracle";
type BlockTransactionResultsProps = {
blockTag: BlockTag;
page?: ProcessedTransaction[];
total: number;
pageNumber: number;
};
const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
blockTag,
page,
total,
pageNumber,
@ -26,6 +30,8 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
const [feeDisplay, feeDisplayToggler] = useFeeToggler();
const { provider } = useContext(RuntimeContext);
const reverseCache = useENSCache(provider, page);
const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
return (
<ContentFrame>
@ -55,6 +61,7 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
tx={tx}
ensCache={reverseCache}
feeDisplay={feeDisplay}
priceMap={priceMap}
/>
))}
<div className="flex justify-between items-baseline py-3">

View File

@ -41,11 +41,13 @@ const DecoratedAddresssLink: React.FC<DecoratedAddressLinkProps> = ({
return (
<div
className={`flex items-baseline space-x-1 ${txFrom ? "bg-red-50" : ""} ${
txTo ? "bg-green-50" : ""
} ${mint ? "italic text-green-500 hover:text-green-700" : ""} ${
burn ? "line-through text-orange-500 hover:text-orange-700" : ""
} ${selfDestruct ? "line-through opacity-70 hover:opacity-100" : ""}`}
className={`flex items-baseline space-x-1 ${
txFrom ? "bg-skin-from" : ""
} ${txTo ? "bg-skin-to" : ""} ${
mint ? "italic text-green-500 hover:text-green-700" : ""
} ${burn ? "line-through text-orange-500 hover:text-orange-700" : ""} ${
selfDestruct ? "line-through opacity-70 hover:opacity-100" : ""
}`}
>
{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 network = provider?.network;
const toMiner = txData.miner !== undefined && internalOp.to === txData.miner;
const toMiner =
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
return (
<>

View File

@ -16,8 +16,11 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
internalOp,
}) => {
const fromMiner =
txData.miner !== undefined && internalOp.from === txData.miner;
const toMiner = txData.miner !== undefined && internalOp.to === txData.miner;
txData.confirmedData?.miner !== undefined &&
internalOp.from === txData.confirmedData.miner;
const toMiner =
txData.confirmedData?.miner !== undefined &&
internalOp.to === txData.confirmedData.miner;
return (
<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 components;
@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"
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>
</div>
</div>

View File

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

View File

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

View File

@ -22,7 +22,7 @@ const BlockRow: React.FC<BlockRowProps> = ({ now, block, baseFeeDelta }) => {
const totalReward = block.blockReward.add(netFeeReward ?? 0);
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>
<BlockLink blockTag={block.number} />
</div>

View File

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

View File

@ -10,8 +10,8 @@ type LogsProps = {
const Logs: React.FC<LogsProps> = ({ txData }) => (
<ContentFrame tabs>
<div className="text-sm py-4">Transaction Receipt Event Logs</div>
{txData &&
txData.logs.map((l, i) => (
{txData.confirmedData &&
txData.confirmedData.logs.map((l, i) => (
<div className="flex space-x-10 py-5" key={i}>
<div>
<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">
<DecoratedAddressLink
address={l.address}
miner={l.address === txData.miner}
miner={l.address === txData.confirmedData?.miner}
txFrom={l.address === txData.from}
txTo={l.address === txData.to}
/>

View File

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

View File

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

View File

@ -48,12 +48,11 @@ export const readBlock = async (
const _rawBlock = await blockPromise;
const _block = provider.formatter.block(_rawBlock.block);
const _rawIssuance = _rawBlock.issuance;
const fees = provider.formatter.bigNumber(_rawBlock.totalFees);
const extBlock: ExtendedBlock = {
blockReward: provider.formatter.bigNumber(_rawIssuance.blockReward ?? 0),
unclesReward: provider.formatter.bigNumber(_rawIssuance.uncleReward ?? 0),
feeReward: fees,
feeReward: provider.formatter.bigNumber(_rawBlock.totalFees),
size: provider.formatter.number(_rawBlock.block.size),
sha3Uncles: _rawBlock.block.sha3Uncles,
stateRoot: _rawBlock.block.stateRoot,
@ -175,8 +174,8 @@ export const useBlockData = (
export const useTxData = (
provider: JsonRpcProvider | undefined,
txhash: string
): TransactionData | undefined => {
const [txData, setTxData] = useState<TransactionData>();
): TransactionData | undefined | null => {
const [txData, setTxData] = useState<TransactionData | undefined | null>();
useEffect(() => {
if (!provider) {
@ -188,24 +187,35 @@ export const useTxData = (
provider.getTransaction(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`;
// Extract token transfers
const tokenTransfers: TokenTransfer[] = [];
for (const l of _receipt.logs) {
if (l.topics.length !== 3) {
continue;
if (_receipt) {
for (const l of _receipt.logs) {
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
@ -228,31 +238,36 @@ export const useTxData = (
}
setTxData({
transactionHash: _receipt.transactionHash,
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
blockTransactionCount: _block.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block.timestamp,
miner: _block.miner,
from: _receipt.from,
to: _receipt.to,
createdContractAddress: _receipt.contractAddress,
transactionHash: _response.hash,
from: _response.from,
to: _response.to,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
blockBaseFeePerGas: _block.baseFeePerGas,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasUsed: _receipt.gasUsed,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
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();
@ -263,13 +278,13 @@ export const useTxData = (
export const useInternalOperations = (
provider: JsonRpcProvider | undefined,
txData: TransactionData | undefined
txData: TransactionData | undefined | null
): InternalOperation[] | undefined => {
const [intTransfers, setIntTransfers] = useState<InternalOperation[]>();
useEffect(() => {
const traceTransfers = async () => {
if (!provider || !txData) {
if (!provider || !txData || !txData.confirmedData) {
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");
function withOpacity(variableName) {
return ({ opacityValue }) => {
if (opacityValue !== undefined) {
return `rgba(var(${variableName}), ${opacityValue})`;
}
return `rgb(var(${variableName}))`;
};
}
module.exports = {
purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
darkMode: false, // or 'media' or 'class'
@ -19,6 +28,28 @@ module.exports = {
balance: ["Fira Code"],
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: {

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