diff --git a/README.md b/README.md index bbfc214..d1aef5b 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,15 @@ This is the preferred way to run Otterscan. You can read about other ways [here] You can make sure it is working correctly if the homepage is able to show the latest block/timestamp your Erigon node is at just bellow the search button. -## Kudos +## Source verification + +We make use of [Sourcify](https://sourcify.dev/) for displaying contract verification info. + +More info [here](docs/sourcify.md). + +## Kudos (in no particular order) + +We make use of many open-source software and integrate many public datasources, mainly: To the [Geth](https://geth.ethereum.org/) team whose code Erigon is based on. @@ -150,6 +158,8 @@ To [Trust Wallet](https://github.com/trustwallet/assets) who sponsor and make av To the owners of the [4bytes repository](https://github.com/ethereum-lists/4bytes) that we import and use to translate the method selectors to human-friendly strings. +To [Sourcify](https://sourcify.dev/), a public decentralized source code and metadata verification service. + To [Ethers](https://github.com/ethers-io/ethers.js/) which is the client library we used to interact with the ETH node. It is high level enough to hide most jsonrpc particularities, but flexible enough to allow easy interaction with custom jsonrpc methods. ## Future @@ -180,4 +190,4 @@ Follow the creator on Twitter for updates ([@wmitsuda](https://twitter.com/wmits ### Donation address -If you like this project, feel free to send donations to `otterscan.eth` +If you like this project, feel free to send donations to `otterscan.eth` or use our gitcoin grant page: https://gitcoin.co/grants/3224/otterscan diff --git a/docs/sourcify.md b/docs/sourcify.md new file mode 100644 index 0000000..cede6d3 --- /dev/null +++ b/docs/sourcify.md @@ -0,0 +1,43 @@ +# Sourcify + +We get the contract source code and metadata from [Sourcify](https://sourcify.dev/). + +There are multiple ways to consume their data we support, each one with pros and cons: + +## IPNS/IPFS + +This is the default integration method, we resolve the public Sourcify IPNS to get the latest known IPFS root hash of their repository. + +The downside is that recently verified contracts may not have yet been added to the root hash and republished into IPNS. + +It assumes a local IPFS gateway at localhost:8080 to avoid leaking your queries to public gateways. + +> This option is actually not working, but it is provided for completeness, follow https://github.com/ethereum/sourcify/issues/495 + +## Direct HTTP connection to Sourcify's repository + +Standard HTTP connection to their repo at https://repo.sourcify.dev/ + +Fast access to fresh verified data. On the other hand it is less private and centralized. + +## Local snapshot + +As a midterm solution, we are making available a snapshot docker image of their repository, containing only mainnet full verified contracts. + +This would allow you to play with existing contracts up to the snapshot date/time locally, not depending on their service or IPFS connectivity availability. + +> It is very likely this run mode will be deprecated in future. + +The Sourcify snapshot is provided as a nginx image at: https://hub.docker.com/repository/docker/otterscan/sourcify-snapshot + +You can run it with: + +``` +docker run --rm -d -p 3006:80 --name sourcify-snapshot otterscan/sourcify-snapshot:2021-09 +``` + +Stop it with: + +``` +docker stop sourcify-snapshot +``` diff --git a/package-lock.json b/package-lock.json index 21ccd93..0669c20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,12 +27,15 @@ "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.24", "@types/node": "^14.17.5", - "@types/react": "^17.0.19", + "@types/react": "^17.0.20", "@types/react-blockies": "^1.4.1", "@types/react-dom": "^17.0.9", + "@types/react-highlight": "^0.12.3", "@types/react-router-dom": "^5.1.8", + "@types/react-syntax-highlighter": "^13.5.2", "chart.js": "^3.5.1", "ethers": "^5.4.1", + "highlightjs-solidity": "^1.2.0", "query-string": "^7.0.1", "react": "^17.0.2", "react-blockies": "^1.4.1", @@ -42,6 +45,7 @@ "react-image": "^4.0.3", "react-router-dom": "^5.2.1", "react-scripts": "4.0.3", + "react-syntax-highlighter": "^15.4.4", "serve": "^12.0.0", "typescript": "^4.4.2", "use-keyboard-shortcut": "^1.0.6", @@ -3004,6 +3008,14 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.2.tgz", + "integrity": "sha512-Op5W7jYgZI7AWKY5wQ0/QNMzQM7dGQPyW1rXKNiymVCy5iTfdPuGu4HhYNOM2sIv8gUfIuIdcYlXmAepwaowow==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/history": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", @@ -3078,9 +3090,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz", - "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==", + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz", + "integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3103,6 +3115,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-highlight": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.3.tgz", + "integrity": "sha512-mfhuHdE3dUjvRv1lvZIvda2B+VW7rkG1ufnFLKbDcRUp/L73bGUmEuEfpnjgdLgeWYho88ahQZRcMSh9GsZA0g==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-router": { "version": "5.1.15", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.15.tgz", @@ -3122,6 +3142,14 @@ "@types/react-router": "*" } }, + "node_modules/@types/react-syntax-highlighter": { + "version": "13.5.2", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-13.5.2.tgz", + "integrity": "sha512-sRZoKZBGKaE7CzMvTTgz+0x/aVR58ZYUTfB7HN76vC+yQnvo1FWtzNARBt0fGqcLGEVakEzMu/CtPzssmanu8Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "0.0.8", "license": "MIT", @@ -3159,6 +3187,11 @@ "source-map": "^0.6.1" } }, + "node_modules/@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, "node_modules/@types/webpack": { "version": "4.41.26", "license": "MIT", @@ -5575,6 +5608,33 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chart.js": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz", @@ -5868,6 +5928,15 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "2.20.3", "license": "MIT" @@ -8426,6 +8495,18 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/faye-websocket": { "version": "0.11.3", "license": "Apache-2.0", @@ -8802,6 +8883,14 @@ "node": ">= 0.12" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/forwarded": { "version": "0.1.2", "license": "MIT", @@ -9196,6 +9285,31 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/he": { "version": "1.2.0", "license": "MIT", @@ -9207,6 +9321,19 @@ "version": "1.1.0", "license": "MIT" }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-solidity": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-1.2.0.tgz", + "integrity": "sha512-KXYcVzBRof3CBWHsxGffsSEAJF0YsPaOk1jgIYv2xSzrBSxkfNUJFXrlE2oZEWvYQKbPqLe4qprJyNbSDV+LZA==" + }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -9756,6 +9883,28 @@ "node": ">=0.10.0" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arguments": { "version": "1.1.0", "license": "MIT", @@ -9856,6 +10005,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-descriptor": { "version": "1.0.2", "license": "MIT", @@ -9933,6 +10091,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-module": { "version": "1.0.0", "license": "MIT" @@ -11300,6 +11467,19 @@ "tslib": "^2.0.3" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "license": "ISC", @@ -12454,6 +12634,23 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse-json": { "version": "5.2.0", "license": "MIT", @@ -13897,6 +14094,11 @@ "node": ">= 0.8" } }, + "node_modules/prismjs": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.1.tgz", + "integrity": "sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==" + }, "node_modules/process": { "version": "0.11.10", "license": "MIT", @@ -13950,6 +14152,18 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.6", "license": "MIT", @@ -14766,6 +14980,21 @@ "node": ">=0.10.0" } }, + "node_modules/react-syntax-highlighter": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.4.4.tgz", + "integrity": "sha512-PsOFHNTzkb3OroXdoR897eKN5EZ6grht1iM+f1lJSq7/L0YVnkJaNVwC3wEUYPOAmeyl5xyer1DjL6MrumO6Zw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.22.0", + "refractor": "^3.2.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "license": "MIT", @@ -14929,6 +15158,20 @@ "postcss-value-parser": "^3.3.0" } }, + "node_modules/refractor": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz", + "integrity": "sha512-dBeD02lC5eytm9Gld2Mx0cMcnR+zhSnsTfPpWqFaMgUMJfC9A6bcN3Br/NaXrnBJcuxnLFR90k1jrkaSyV8umg==", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.24.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/regenerate": { "version": "1.4.2", "license": "MIT" @@ -16461,6 +16704,15 @@ "version": "1.4.8", "license": "MIT" }, + "node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/spdx-correct": { "version": "3.1.1", "license": "Apache-2.0", @@ -21378,6 +21630,14 @@ "@types/node": "*" } }, + "@types/hast": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.2.tgz", + "integrity": "sha512-Op5W7jYgZI7AWKY5wQ0/QNMzQM7dGQPyW1rXKNiymVCy5iTfdPuGu4HhYNOM2sIv8gUfIuIdcYlXmAepwaowow==", + "requires": { + "@types/unist": "*" + } + }, "@types/history": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", @@ -21440,9 +21700,9 @@ "version": "1.5.4" }, "@types/react": { - "version": "17.0.19", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.19.tgz", - "integrity": "sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==", + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.20.tgz", + "integrity": "sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -21465,6 +21725,14 @@ "@types/react": "*" } }, + "@types/react-highlight": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.3.tgz", + "integrity": "sha512-mfhuHdE3dUjvRv1lvZIvda2B+VW7rkG1ufnFLKbDcRUp/L73bGUmEuEfpnjgdLgeWYho88ahQZRcMSh9GsZA0g==", + "requires": { + "@types/react": "*" + } + }, "@types/react-router": { "version": "5.1.15", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.15.tgz", @@ -21484,6 +21752,14 @@ "@types/react-router": "*" } }, + "@types/react-syntax-highlighter": { + "version": "13.5.2", + "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-13.5.2.tgz", + "integrity": "sha512-sRZoKZBGKaE7CzMvTTgz+0x/aVR58ZYUTfB7HN76vC+yQnvo1FWtzNARBt0fGqcLGEVakEzMu/CtPzssmanu8Q==", + "requires": { + "@types/react": "*" + } + }, "@types/resolve": { "version": "0.0.8", "requires": { @@ -21514,6 +21790,11 @@ "source-map": "^0.6.1" } }, + "@types/unist": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", + "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==" + }, "@types/webpack": { "version": "4.41.26", "requires": { @@ -23186,6 +23467,21 @@ "char-regex": { "version": "1.0.2" }, + "character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==" + }, + "character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==" + }, + "character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" + }, "chart.js": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz", @@ -23403,6 +23699,11 @@ "delayed-stream": "~1.0.0" } }, + "comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==" + }, "commander": { "version": "2.20.3" }, @@ -25106,6 +25407,14 @@ "reusify": "^1.0.4" } }, + "fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "requires": { + "format": "^0.2.0" + } + }, "faye-websocket": { "version": "0.11.3", "requires": { @@ -25349,6 +25658,11 @@ "mime-types": "^2.1.12" } }, + "format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=" + }, "forwarded": { "version": "0.1.2" }, @@ -25599,12 +25913,39 @@ "minimalistic-assert": "^1.0.1" } }, + "hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==" + }, + "hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "requires": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + } + }, "he": { "version": "1.2.0" }, "hex-color-regex": { "version": "1.1.0" }, + "highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" + }, + "highlightjs-solidity": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-1.2.0.tgz", + "integrity": "sha512-KXYcVzBRof3CBWHsxGffsSEAJF0YsPaOk1jgIYv2xSzrBSxkfNUJFXrlE2oZEWvYQKbPqLe4qprJyNbSDV+LZA==" + }, "history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -25972,6 +26313,20 @@ } } }, + "is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==" + }, + "is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "requires": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + } + }, "is-arguments": { "version": "1.1.0", "requires": { @@ -26030,6 +26385,11 @@ "is-date-object": { "version": "1.0.2" }, + "is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==" + }, "is-descriptor": { "version": "1.0.2", "requires": { @@ -26067,6 +26427,11 @@ "is-extglob": "^2.1.1" } }, + "is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" + }, "is-module": { "version": "1.0.0" }, @@ -26976,6 +27341,15 @@ "tslib": "^2.0.3" } }, + "lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "requires": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + } + }, "lru-cache": { "version": "6.0.0", "requires": { @@ -27720,6 +28094,19 @@ "safe-buffer": "^5.1.1" } }, + "parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, "parse-json": { "version": "5.2.0", "requires": { @@ -28708,6 +29095,11 @@ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, + "prismjs": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.24.1.tgz", + "integrity": "sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow==" + }, "process": { "version": "0.11.10" }, @@ -28746,6 +29138,14 @@ } } }, + "property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "requires": { + "xtend": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.6", "requires": { @@ -29309,6 +29709,18 @@ } } }, + "react-syntax-highlighter": { + "version": "15.4.4", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.4.4.tgz", + "integrity": "sha512-PsOFHNTzkb3OroXdoR897eKN5EZ6grht1iM+f1lJSq7/L0YVnkJaNVwC3wEUYPOAmeyl5xyer1DjL6MrumO6Zw==", + "requires": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "lowlight": "^1.17.0", + "prismjs": "^1.22.0", + "refractor": "^3.2.0" + } + }, "read-pkg": { "version": "5.2.0", "requires": { @@ -29428,6 +29840,16 @@ "postcss-value-parser": "^3.3.0" } }, + "refractor": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.4.0.tgz", + "integrity": "sha512-dBeD02lC5eytm9Gld2Mx0cMcnR+zhSnsTfPpWqFaMgUMJfC9A6bcN3Br/NaXrnBJcuxnLFR90k1jrkaSyV8umg==", + "requires": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.24.0" + } + }, "regenerate": { "version": "1.4.2" }, @@ -30492,6 +30914,11 @@ "sourcemap-codec": { "version": "1.4.8" }, + "space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==" + }, "spdx-correct": { "version": "3.1.1", "requires": { @@ -32583,4 +33010,4 @@ "version": "0.1.0" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index fb6e41e..d2925fe 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,15 @@ "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.24", "@types/node": "^14.17.5", - "@types/react": "^17.0.19", + "@types/react": "^17.0.20", "@types/react-blockies": "^1.4.1", "@types/react-dom": "^17.0.9", + "@types/react-highlight": "^0.12.3", "@types/react-router-dom": "^5.1.8", + "@types/react-syntax-highlighter": "^13.5.2", "chart.js": "^3.5.1", "ethers": "^5.4.1", + "highlightjs-solidity": "^1.2.0", "query-string": "^7.0.1", "react": "^17.0.2", "react-blockies": "^1.4.1", @@ -37,6 +40,7 @@ "react-image": "^4.0.3", "react-router-dom": "^5.2.1", "react-scripts": "4.0.3", + "react-syntax-highlighter": "^15.4.4", "serve": "^12.0.0", "typescript": "^4.4.2", "use-keyboard-shortcut": "^1.0.6", diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index f4fd2ff..45a07ca 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -1,13 +1,26 @@ import React, { useState, useEffect, useMemo, useContext } from "react"; -import { useParams, useLocation, useHistory } from "react-router-dom"; +import { + useParams, + useLocation, + useHistory, + Switch, + Route, +} from "react-router-dom"; import { BlockTag } from "@ethersproject/abstract-provider"; import { getAddress, isAddress } from "@ethersproject/address"; +import { Tab } from "@headlessui/react"; import queryString from "query-string"; import Blockies from "react-blockies"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch"; +import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle"; +import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle"; import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; import Copy from "./components/Copy"; import ContentFrame from "./ContentFrame"; +import NavTab from "./components/NavTab"; +import Contracts from "./address/Contracts"; import UndefinedPageControl from "./search/UndefinedPageControl"; import ResultHeader from "./search/ResultHeader"; import PendingResults from "./search/PendingResults"; @@ -18,6 +31,8 @@ import { useENSCache } from "./useReverseCache"; import { useFeeToggler } from "./search/useFeeToggler"; import { SelectionContext, useSelection } from "./useSelection"; import { useMultipleETHUSDOracle } from "./usePriceOracle"; +import { useSourcify } from "./useSourcify"; +import { SourcifySource } from "./url"; type BlockParams = { addressOrName: string; @@ -165,6 +180,14 @@ const AddressTransactions: React.FC = () => { const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const selectionCtx = useSelection(); + const [sourcifySource, setSourcifySource] = useState( + SourcifySource.IPFS_IPNS + ); + const rawMetadata = useSourcify( + checksummedAddress, + provider?.network.chainId, + sourcifySource + ); return ( @@ -194,59 +217,112 @@ const AddressTransactions: React.FC = () => { )} - -
-
- {page === undefined ? ( - <>Waiting for search results... - ) : ( - <>{page.length} transactions on this page - )} -
- -
- - {controller ? ( - - {controller.getPage().map((tx) => ( - - ))} -
-
- {page !== undefined && ( - <>{page.length} transactions on this page + + + + Overview + + + + Contract + {rawMetadata === undefined ? ( + + + + ) : rawMetadata === null ? ( + + + + ) : ( + + + + )} + + + + + + + +
+
+ {page === undefined ? ( + <>Waiting for search results... + ) : ( + <>{page.length} transactions on this page + )} +
+ +
+ + {controller ? ( + + {controller.getPage().map((tx) => ( + + ))} +
+
+ {page === undefined ? ( + <>Waiting for search results... + ) : ( + <>{page.length} transactions on this page + )} +
+ +
+ +
+ ) : ( + )} -
- + + + -
-
- ) : ( - - )} -
+ + + + ) )} diff --git a/src/Block.tsx b/src/Block.tsx index caa309b..7f9e552 100644 --- a/src/Block.tsx +++ b/src/Block.tsx @@ -165,7 +165,9 @@ const Block: React.FC = () => { - {commify(block.difficulty)} + + {block.difficulty ? commify(block.difficulty) : "?"} + {commify(block.totalDifficulty.toString())} diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 5d10101..e204633 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -1,9 +1,10 @@ import React, { useMemo, useContext } from "react"; import { Route, Switch, useParams } from "react-router-dom"; +import { Tab } from "@headlessui/react"; import StandardFrame from "./StandardFrame"; import StandardSubtitle from "./StandardSubtitle"; import ContentFrame from "./ContentFrame"; -import Tab from "./components/Tab"; +import NavTab from "./components/NavTab"; import Details from "./transaction/Details"; import Logs from "./transaction/Logs"; import { RuntimeContext } from "./useRuntime"; @@ -55,14 +56,17 @@ const Transaction: React.FC = () => { )} {txData && ( -
- Overview - {txData.confirmedData?.blockNumber !== undefined && ( - - Logs{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} - - )} -
+ + + Overview + {txData.confirmedData?.blockNumber !== undefined && ( + + Logs + {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} + + )} + +
= ({ abi }) => ( + + {JSON.stringify(abi, null, " ") ?? ""} + +); + +export default React.memo(ABI); diff --git a/src/address/Contract.tsx b/src/address/Contract.tsx new file mode 100644 index 0000000..eece5fd --- /dev/null +++ b/src/address/Contract.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; +import hljs from "highlight.js"; +import docco from "react-syntax-highlighter/dist/esm/styles/hljs/docco"; +import { useContract } from "../useSourcify"; +import { SourcifySource } from "../url"; + +import hljsDefineSolidity from "highlightjs-solidity"; +hljsDefineSolidity(hljs); + +type ContractProps = { + checksummedAddress: string; + networkId: number; + filename: string; + source: any; + sourcifySource: SourcifySource; +}; + +const Contract: React.FC = ({ + checksummedAddress, + networkId, + filename, + source, + sourcifySource, +}) => { + const content = useContract( + checksummedAddress, + networkId, + filename, + source, + sourcifySource + ); + + return ( + + {content ?? ""} + + ); +}; + +export default React.memo(Contract); diff --git a/src/address/Contracts.tsx b/src/address/Contracts.tsx new file mode 100644 index 0000000..4d322c3 --- /dev/null +++ b/src/address/Contracts.tsx @@ -0,0 +1,159 @@ +import React, { useState, useEffect, useContext, Fragment } from "react"; +import { commify } from "@ethersproject/units"; +import { Menu, RadioGroup } from "@headlessui/react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; +import ContentFrame from "../ContentFrame"; +import InfoRow from "../components/InfoRow"; +import Copy from "../components/Copy"; +import ABI from "./ABI"; +import Contract from "./Contract"; +import { RuntimeContext } from "../useRuntime"; +import { Metadata } from "../useSourcify"; +import ExternalLink from "../components/ExternalLink"; +import { openInRemixURL, SourcifySource } from "../url"; +import RadioButton from "./RadioButton"; + +type ContractsProps = { + checksummedAddress: string; + rawMetadata: Metadata | null | undefined; + sourcifySource: SourcifySource; + setSourcifySource: (sourcifySource: SourcifySource) => void; +}; + +const Contracts: React.FC = ({ + checksummedAddress, + rawMetadata, + sourcifySource, + setSourcifySource, +}) => { + const { provider } = useContext(RuntimeContext); + + const [selected, setSelected] = useState(); + useEffect(() => { + if (rawMetadata) { + setSelected(Object.keys(rawMetadata.sources)[0]); + } + }, [rawMetadata]); + const optimizer = rawMetadata?.settings?.optimizer; + + return ( + + + +
+ + Resolve IPNS @localhost:8080 gateway + + + Sourcify Servers + + + Local Snapshot @localhost:3006 + +
+
+
+ {rawMetadata && ( + <> + + {rawMetadata.language} + + + {rawMetadata.compiler.version} + + + {optimizer?.enabled ? ( + + Yes with{" "} + + {commify(optimizer?.runs)} + {" "} + runs + + ) : ( + No + )} + + + )} +
+ {rawMetadata === undefined && ( + Getting data from Sourcify repository... + )} + {rawMetadata === null && ( + + Address is not a contract or couldn't find contract metadata in + Sourcify repository. + + )} + {rawMetadata !== undefined && rawMetadata !== null && ( + <> + {rawMetadata.output.abi && ( +
+
+ ABI + +
+ +
+ )} +
+ +
+ + {selected} + + + + + {provider && ( +
+ + Open in Remix + +
+ )} +
+
+ + {Object.entries(rawMetadata.sources).map(([k]) => ( + + + + ))} + +
+
+ {selected && ( + + )} +
+ + )} +
+
+ ); +}; + +export default React.memo(Contracts); diff --git a/src/address/RadioButton.tsx b/src/address/RadioButton.tsx new file mode 100644 index 0000000..e93fad3 --- /dev/null +++ b/src/address/RadioButton.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { RadioGroup } from "@headlessui/react"; +import { SourcifySource } from "../url"; + +type RadioButtonProps = { + value: SourcifySource; +}; + +const RadioButton: React.FC = ({ value, children }) => ( + + `border rounded px-2 py-1 cursor-pointer ${ + checked + ? "bg-blue-400 hover:bg-blue-500 text-white" + : "hover:bg-gray-200" + }` + } + value={value} + > + {children} + +); + +export default RadioButton; diff --git a/src/components/NavTab.tsx b/src/components/NavTab.tsx new file mode 100644 index 0000000..83cf6d8 --- /dev/null +++ b/src/components/NavTab.tsx @@ -0,0 +1,23 @@ +import React, { Fragment } from "react"; +import { NavLink } from "react-router-dom"; +import { Tab } from "@headlessui/react"; + +type NavTabProps = { + href: string; +}; + +const NavTab: React.FC = ({ href, children }) => ( + + + {children} + + +); + +export default NavTab; diff --git a/src/components/Tab.tsx b/src/components/Tab.tsx deleted file mode 100644 index 4e9352a..0000000 --- a/src/components/Tab.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import { NavLink } from "react-router-dom"; - -type TabProps = { - href: string; -}; - -const Tab: React.FC = ({ href, children }) => ( - - {children} - -); - -export default Tab; diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 58b3bbc..625fe40 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -1,2 +1,3 @@ /// declare module "use-keyboard-shortcut"; +declare module "highlightjs-solidity"; diff --git a/src/url.ts b/src/url.ts index 00e3dc8..9b80e34 100644 --- a/src/url.ts +++ b/src/url.ts @@ -13,3 +13,52 @@ export const tokenLogoURL = ( export const blockURL = (blockNum: BlockTag) => `/block/${blockNum}`; export const blockTxsURL = (blockNum: BlockTag) => `/block/${blockNum}/txs`; + +export enum SourcifySource { + // Resolve trusted IPNS for root IPFS + IPFS_IPNS, + + // Centralized Sourcify servers + CENTRAL_SERVER, + + // Snapshot server + CUSTOM_SNAPSHOT_SERVER, +} + +const sourcifyIPNS = + "k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p"; +const ipfsGatewayPrefix = `http://localhost:8080/ipns/${sourcifyIPNS}`; +const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`; +const snapshotPrefix = "http://localhost:3006"; + +const resolveSourcifySource = (source: SourcifySource) => { + if (source === SourcifySource.IPFS_IPNS) { + return ipfsGatewayPrefix; + } + if (source === SourcifySource.CENTRAL_SERVER) { + return sourcifyHttpRepoPrefix; + } + return snapshotPrefix; +}; + +export const sourcifyMetadata = ( + checksummedAddress: string, + networkId: number, + source: SourcifySource +) => + `${resolveSourcifySource( + source + )}/contracts/full_match/${networkId}/${checksummedAddress}/metadata.json`; + +export const sourcifySourceFile = ( + checksummedAddress: string, + networkId: number, + filepath: string, + source: SourcifySource +) => + `${resolveSourcifySource( + source + )}/contracts/full_match/${networkId}/${checksummedAddress}/sources/${filepath}`; + +export const openInRemixURL = (checksummedAddress: string, networkId: number) => + `https://remix.ethereum.org/#call=source-verification//fetchAndSave//${checksummedAddress}//${networkId}`; diff --git a/src/useSourcify.ts b/src/useSourcify.ts new file mode 100644 index 0000000..e8806b9 --- /dev/null +++ b/src/useSourcify.ts @@ -0,0 +1,109 @@ +import { useState, useEffect } from "react"; +import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "./url"; + +export type Metadata = { + version: string; + language: string; + compiler: { + version: string; + keccak256?: string | undefined; + }; + sources: { + [filename: string]: { + keccak256: string; + content?: string | undefined; + urls?: string[]; + license?: string; + }; + }; + settings: { + remappings: string[]; + optimizer?: { + enabled: boolean; + runs: number; + }; + compilationTarget: { + [filename: string]: string; + }; + libraries: { + [filename: string]: string; + }; + }; + output: { + abi: any[]; + userdocs: any[]; + devdoc: any[]; + }; +}; + +export const useSourcify = ( + checksummedAddress: string | undefined, + chainId: number | undefined, + source: SourcifySource +) => { + const [rawMetadata, setRawMetadata] = useState(); + + useEffect(() => { + if (!checksummedAddress || chainId === undefined) { + return; + } + + setRawMetadata(undefined); + const fetchMetadata = async () => { + try { + const contractMetadataURL = sourcifyMetadata( + checksummedAddress, + chainId, + source + ); + const result = await fetch(contractMetadataURL); + if (result.ok) { + const _metadata = await result.json(); + setRawMetadata(_metadata); + } else { + setRawMetadata(null); + } + } catch (err) { + console.error(err); + setRawMetadata(null); + } + }; + fetchMetadata(); + }, [checksummedAddress, chainId, source]); + + return rawMetadata; +}; + +export const useContract = ( + checksummedAddress: string, + networkId: number, + filename: string, + source: any, + sourcifySource: SourcifySource +) => { + const [content, setContent] = useState(source.content); + + useEffect(() => { + if (source.content) { + return; + } + + const readContent = async () => { + const normalizedFilename = filename.replaceAll(/[@:]/g, "_"); + const url = sourcifySourceFile( + checksummedAddress, + networkId, + normalizedFilename, + sourcifySource + ); + const res = await fetch(url); + if (res.ok) { + const _content = await res.text(); + setContent(_content); + } + }; + readContent(); + }, [checksummedAddress, networkId, filename, source.content, sourcifySource]); + + return content; +}; diff --git a/tailwind.config.js b/tailwind.config.js index a6eafd8..012af47 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -27,6 +27,7 @@ module.exports = { data: ["Roboto Mono"], balance: ["Fira Code"], blocknum: ["Roboto"], + code: ["Fira Code"], }, borderColor: { skin: {