Merge branch 'release/v2021.09.02-otterscan'

This commit is contained in:
Willian Mitsuda 2021-09-13 15:14:10 -03:00
commit 1688a8662b
17 changed files with 1075 additions and 93 deletions

View File

@ -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

43
docs/sourcify.md Normal file
View File

@ -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
```

443
package-lock.json generated
View File

@ -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"
}
}
}
}

View File

@ -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",

View File

@ -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>(
SourcifySource.IPFS_IPNS
);
const rawMetadata = useSourcify(
checksummedAddress,
provider?.network.chainId,
sourcifySource
);
return (
<StandardFrame>
@ -194,59 +217,112 @@ const AddressTransactions: React.FC = () => {
)}
</div>
</StandardSubtitle>
<ContentFrame>
<div className="flex justify-between items-baseline py-3">
<div className="text-sm text-gray-500">
{page === undefined ? (
<>Waiting for search results...</>
) : (
<>{page.length} transactions on this page</>
)}
</div>
<UndefinedPageControl
address={params.addressOrName}
isFirst={controller?.isFirst}
isLast={controller?.isLast}
prevHash={page ? page[0].hash : ""}
nextHash={page ? page[page.length - 1].hash : ""}
disabled={controller === undefined}
/>
</div>
<ResultHeader
feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler}
/>
{controller ? (
<SelectionContext.Provider value={selectionCtx}>
{controller.getPage().map((tx) => (
<TransactionItem
key={tx.hash}
tx={tx}
ensCache={reverseCache}
selectedAddress={checksummedAddress}
feeDisplay={feeDisplay}
priceMap={priceMap}
/>
))}
<div className="flex justify-between items-baseline py-3">
<div className="text-sm text-gray-500">
{page !== undefined && (
<>{page.length} transactions on this page</>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href={`/address/${checksummedAddress}`}>
Overview
</NavTab>
<NavTab href={`/address/${checksummedAddress}/contract`}>
<span
className={`flex items-baseline space-x-2 ${
rawMetadata === undefined ? "italic opacity-50" : ""
}`}
>
<span>Contract</span>
{rawMetadata === undefined ? (
<span className="self-center">
<FontAwesomeIcon
className="animate-spin"
icon={faCircleNotch}
/>
</span>
) : rawMetadata === null ? (
<span className="self-center text-red-500">
<FontAwesomeIcon icon={faQuestionCircle} />
</span>
) : (
<span className="self-center text-green-500">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
)}
</span>
</NavTab>
</Tab.List>
<Tab.Panels>
<Switch>
<Route path="/address/:addressOrName" exact>
<ContentFrame tabs>
<div className="flex justify-between items-baseline py-3">
<div className="text-sm text-gray-500">
{page === undefined ? (
<>Waiting for search results...</>
) : (
<>{page.length} transactions on this page</>
)}
</div>
<UndefinedPageControl
address={params.addressOrName}
isFirst={controller?.isFirst}
isLast={controller?.isLast}
prevHash={page ? page[0].hash : ""}
nextHash={page ? page[page.length - 1].hash : ""}
disabled={controller === undefined}
/>
</div>
<ResultHeader
feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler}
/>
{controller ? (
<SelectionContext.Provider value={selectionCtx}>
{controller.getPage().map((tx) => (
<TransactionItem
key={tx.hash}
tx={tx}
ensCache={reverseCache}
selectedAddress={checksummedAddress}
feeDisplay={feeDisplay}
priceMap={priceMap}
/>
))}
<div className="flex justify-between items-baseline py-3">
<div className="text-sm text-gray-500">
{page === undefined ? (
<>Waiting for search results...</>
) : (
<>{page.length} transactions on this page</>
)}
</div>
<UndefinedPageControl
address={params.addressOrName}
isFirst={controller?.isFirst}
isLast={controller?.isLast}
prevHash={page ? page[0].hash : ""}
nextHash={page ? page[page.length - 1].hash : ""}
disabled={controller === undefined}
/>
</div>
<ResultHeader
feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler}
/>
</SelectionContext.Provider>
) : (
<PendingResults />
)}
</div>
<UndefinedPageControl
address={params.addressOrName}
isFirst={controller.isFirst}
isLast={controller.isLast}
prevHash={page ? page[0].hash : ""}
nextHash={page ? page[page.length - 1].hash : ""}
</ContentFrame>
</Route>
<Route path="/address/:addressOrName/contract" exact>
<Contracts
checksummedAddress={checksummedAddress}
rawMetadata={rawMetadata}
sourcifySource={sourcifySource}
setSourcifySource={setSourcifySource}
/>
</div>
</SelectionContext.Provider>
) : (
<PendingResults />
)}
</ContentFrame>
</Route>
</Switch>
</Tab.Panels>
</Tab.Group>
</>
)
)}

View File

@ -165,7 +165,9 @@ const Block: React.FC = () => {
<InfoRow title="Ether Price">
<USDValue value={blockETHUSDPrice} />
</InfoRow>
<InfoRow title="Difficult">{commify(block.difficulty)}</InfoRow>
<InfoRow title="Difficult">
{block.difficulty ? commify(block.difficulty) : "?"}
</InfoRow>
<InfoRow title="Total Difficult">
{commify(block.totalDifficulty.toString())}
</InfoRow>

View File

@ -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 && (
<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>
{txData.confirmedData?.blockNumber !== undefined && (
<Tab href={`/tx/${txhash}/logs`}>
Logs{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</Tab>
)}
</div>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href={`/tx/${txhash}`}>Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href={`/tx/${txhash}/logs`}>
Logs
{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
</Tab.List>
</Tab.Group>
<Switch>
<Route path="/tx/:txhash/" exact>
<Details

24
src/address/ABI.tsx Normal file
View File

@ -0,0 +1,24 @@
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 hljsDefineSolidity from "highlightjs-solidity";
hljsDefineSolidity(hljs);
type ABIProps = {
abi: any[];
};
const ABI: React.FC<ABIProps> = ({ abi }) => (
<SyntaxHighlighter
className="w-full h-60 border font-code text-base"
language="json"
style={docco}
showLineNumbers
>
{JSON.stringify(abi, null, " ") ?? ""}
</SyntaxHighlighter>
);
export default React.memo(ABI);

46
src/address/Contract.tsx Normal file
View File

@ -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<ContractProps> = ({
checksummedAddress,
networkId,
filename,
source,
sourcifySource,
}) => {
const content = useContract(
checksummedAddress,
networkId,
filename,
source,
sourcifySource
);
return (
<SyntaxHighlighter
className="w-full h-full border font-code text-base"
language="solidity"
style={docco}
showLineNumbers
>
{content ?? ""}
</SyntaxHighlighter>
);
};
export default React.memo(Contract);

159
src/address/Contracts.tsx Normal file
View File

@ -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<ContractsProps> = ({
checksummedAddress,
rawMetadata,
sourcifySource,
setSourcifySource,
}) => {
const { provider } = useContext(RuntimeContext);
const [selected, setSelected] = useState<string>();
useEffect(() => {
if (rawMetadata) {
setSelected(Object.keys(rawMetadata.sources)[0]);
}
}, [rawMetadata]);
const optimizer = rawMetadata?.settings?.optimizer;
return (
<ContentFrame tabs>
<InfoRow title="Sourcify integration">
<RadioGroup value={sourcifySource} onChange={setSourcifySource}>
<div className="flex space-x-2">
<RadioButton value={SourcifySource.IPFS_IPNS}>
Resolve IPNS @localhost:8080 gateway
</RadioButton>
<RadioButton value={SourcifySource.CENTRAL_SERVER}>
Sourcify Servers
</RadioButton>
<RadioButton value={SourcifySource.CUSTOM_SNAPSHOT_SERVER}>
Local Snapshot @localhost:3006
</RadioButton>
</div>
</RadioGroup>
</InfoRow>
{rawMetadata && (
<>
<InfoRow title="Language">
<span>{rawMetadata.language}</span>
</InfoRow>
<InfoRow title="Compiler">
<span>{rawMetadata.compiler.version}</span>
</InfoRow>
<InfoRow title="Optimizer Enabled">
{optimizer?.enabled ? (
<span>
<span className="font-bold text-green-600">Yes</span> with{" "}
<span className="font-bold text-green-600">
{commify(optimizer?.runs)}
</span>{" "}
runs
</span>
) : (
<span className="font-bold text-red-600">No</span>
)}
</InfoRow>
</>
)}
<div className="py-5">
{rawMetadata === undefined && (
<span>Getting data from Sourcify repository...</span>
)}
{rawMetadata === null && (
<span>
Address is not a contract or couldn't find contract metadata in
Sourcify repository.
</span>
)}
{rawMetadata !== undefined && rawMetadata !== null && (
<>
{rawMetadata.output.abi && (
<div className="mb-3">
<div className="flex space-x-2 text-sm border-l border-r border-t rounded-t px-2 py-1">
<span>ABI</span>
<Copy value={JSON.stringify(rawMetadata.output.abi)} />
</div>
<ABI abi={rawMetadata.output.abi} />
</div>
)}
<div>
<Menu>
<div className="flex space-x-2 justify-between items-baseline">
<Menu.Button className="flex space-x-2 text-sm border-l border-r border-t rounded-t px-2 py-1">
<span>{selected}</span>
<span className="self-center">
<FontAwesomeIcon icon={faChevronDown} size="xs" />
</span>
</Menu.Button>
{provider && (
<div className="text-sm">
<ExternalLink
href={openInRemixURL(
checksummedAddress,
provider.network.chainId
)}
>
Open in Remix
</ExternalLink>
</div>
)}
</div>
<div className="relative">
<Menu.Items className="absolute border p-1 rounded-b bg-white flex flex-col">
{Object.entries(rawMetadata.sources).map(([k]) => (
<Menu.Item key={k}>
<button
className={`flex text-sm px-2 py-1 ${
selected === k
? "font-bold bg-gray-200 text-gray-500"
: "hover:border-orange-200 hover:text-gray-500 text-gray-400 transition-transform transition-colors duration-75"
}`}
onClick={() => setSelected(k)}
>
{k}
</button>
</Menu.Item>
))}
</Menu.Items>
</div>
</Menu>
{selected && (
<Contract
checksummedAddress={checksummedAddress}
networkId={provider!.network.chainId}
filename={selected}
source={rawMetadata.sources[selected]}
sourcifySource={sourcifySource}
/>
)}
</div>
</>
)}
</div>
</ContentFrame>
);
};
export default React.memo(Contracts);

View File

@ -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<RadioButtonProps> = ({ value, children }) => (
<RadioGroup.Option
className={({ checked }) =>
`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}
</RadioGroup.Option>
);
export default RadioButton;

23
src/components/NavTab.tsx Normal file
View File

@ -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<NavTabProps> = ({ href, children }) => (
<Tab as={Fragment}>
<NavLink
className="text-gray-500 border-transparent hover:text-link-blue text-sm font-bold px-3 py-3 border-b-2"
activeClassName="text-link-blue border-link-blue"
to={href}
exact
replace
>
{children}
</NavLink>
</Tab>
);
export default NavTab;

View File

@ -1,20 +0,0 @@
import React from "react";
import { NavLink } from "react-router-dom";
type TabProps = {
href: string;
};
const Tab: React.FC<TabProps> = ({ href, children }) => (
<NavLink
className="text-gray-500 border-transparent hover:text-link-blue text-sm font-bold px-3 py-3 border-b-2"
activeClassName="text-link-blue border-link-blue"
to={href}
exact
replace
>
{children}
</NavLink>
);
export default Tab;

View File

@ -1,2 +1,3 @@
/// <reference types="react-scripts" />
declare module "use-keyboard-shortcut";
declare module "highlightjs-solidity";

View File

@ -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}`;

109
src/useSourcify.ts Normal file
View File

@ -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<Metadata | null | undefined>();
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<string>(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;
};

View File

@ -27,6 +27,7 @@ module.exports = {
data: ["Roboto Mono"],
balance: ["Fira Code"],
blocknum: ["Roboto"],
code: ["Fira Code"],
},
borderColor: {
skin: {