diff --git a/package-lock.json b/package-lock.json index 0669c20..9d80934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@blackbox-vision/react-qr-reader": "^5.0.0", "@chainlink/contracts": "^0.2.1", - "@craco/craco": "^6.2.0", + "@craco/craco": "^6.3.0", "@fontsource/fira-code": "^4.5.1", "@fontsource/roboto": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0", @@ -34,7 +34,7 @@ "@types/react-router-dom": "^5.1.8", "@types/react-syntax-highlighter": "^13.5.2", "chart.js": "^3.5.1", - "ethers": "^5.4.1", + "ethers": "^5.4.6", "highlightjs-solidity": "^1.2.0", "query-string": "^7.0.1", "react": "^17.0.2", @@ -43,11 +43,11 @@ "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", "react-image": "^4.0.3", - "react-router-dom": "^5.2.1", + "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", "react-syntax-highlighter": "^15.4.4", - "serve": "^12.0.0", - "typescript": "^4.4.2", + "serve": "^12.0.1", + "typescript": "^4.4.3", "use-keyboard-shortcut": "^1.0.6", "web-vitals": "^1.0.1" }, @@ -1247,9 +1247,9 @@ } }, "node_modules/@craco/craco": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.2.0.tgz", - "integrity": "sha512-kLc4GSdgR9D5JiZmSxtzbvBKcUFSJqMXImRjjYf5pacwiyAs3XfQwai7T+pExfLQNUnytgkL8jRFUJeYrkVr7g==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.3.0.tgz", + "integrity": "sha512-SCnfEQxT/6NAbU/3sIWw7gQXtzjjiTp/EZFdJTd8inPURILIy0YajrC2p8qBG2KhFo5cwgOrEDyaGyAFvvuyuA==", "dependencies": { "cross-spawn": "^7.0.0", "lodash": "^4.17.15", @@ -1324,9 +1324,9 @@ } }, "node_modules/@ethersproject/abi": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.4.0.tgz", - "integrity": "sha512-9gU2H+/yK1j2eVMdzm6xvHSnMxk8waIHQGYCZg5uvAyH0rsAzxkModzBSpbAkAuhKFEovC2S9hM4nPuLym8IZw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.4.1.tgz", + "integrity": "sha512-9mhbjUk76BiSluiiW4BaYyI58KSbDMMQpCLdsAR+RsT2GyATiNYxVv+pGWRrekmsIdY3I+hOqsYQSTkc8L/mcg==", "funding": [ { "type": "individual", @@ -1350,9 +1350,9 @@ } }, "node_modules/@ethersproject/abstract-provider": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.4.0.tgz", - "integrity": "sha512-vPBR7HKUBY0lpdllIn7tLIzNN7DrVnhCLKSzY0l8WAwxz686m/aL7ASDzrVxV93GJtIub6N2t4dfZ29CkPOxgA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.4.1.tgz", + "integrity": "sha512-3EedfKI3LVpjSKgAxoUaI+gB27frKsxzm+r21w9G60Ugk+3wVLQwhi1LsEJAKNV7WoZc8CIpNrATlL1QFABjtQ==", "funding": [ { "type": "individual", @@ -1511,9 +1511,9 @@ } }, "node_modules/@ethersproject/contracts": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.4.0.tgz", - "integrity": "sha512-hkO3L3IhS1Z3ZtHtaAG/T87nQ7KiPV+/qnvutag35I0IkiQ8G3ZpCQ9NNOpSCzn4pWSW4CfzmtE02FcqnLI+hw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.4.1.tgz", + "integrity": "sha512-m+z2ZgPy4pyR15Je//dUaymRUZq5MtDajF6GwFbGAVmKz/RF+DNIPwF0k5qEcL3wPGVqUjFg2/krlCRVTU4T5w==", "funding": [ { "type": "individual", @@ -1641,9 +1641,9 @@ } }, "node_modules/@ethersproject/logger": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.4.0.tgz", - "integrity": "sha512-xYdWGGQ9P2cxBayt64d8LC8aPFJk6yWCawQi/4eJ4+oJdMMjEBMrIcIMZ9AxhwpPVmnBPrsB10PcXGmGAqgUEQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.4.1.tgz", + "integrity": "sha512-DZ+bRinnYLPw1yAC64oRl0QyVZj43QeHIhVKfD/+YwSz4wsv1pfwb5SOFjz+r710YEWzU6LrhuSjpSO+6PeE4A==", "funding": [ { "type": "individual", @@ -1656,9 +1656,9 @@ ] }, "node_modules/@ethersproject/networks": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.4.1.tgz", - "integrity": "sha512-8SvowCKz9Uf4xC5DTKI8+il8lWqOr78kmiqAVLYT9lzB8aSmJHQMD1GSuJI0CW4hMAnzocpGpZLgiMdzsNSPig==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.4.2.tgz", + "integrity": "sha512-eekOhvJyBnuibfJnhtK46b8HimBc5+4gqpvd1/H9LEl7Q7/qhsIhM81dI9Fcnjpk3jB1aTy6bj0hz3cifhNeYw==", "funding": [ { "type": "individual", @@ -1693,9 +1693,9 @@ } }, "node_modules/@ethersproject/properties": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.4.0.tgz", - "integrity": "sha512-7jczalGVRAJ+XSRvNA6D5sAwT4gavLq3OXPuV/74o3Rd2wuzSL035IMpIMgei4CYyBdialJMrTqkOnzccLHn4A==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.4.1.tgz", + "integrity": "sha512-cyCGlF8wWlIZyizsj2PpbJ9I7rIlUAfnHYwy/T90pdkSn/NFTa5YWZx2wTJBe9V7dD65dcrrEMisCRUJiq6n3w==", "funding": [ { "type": "individual", @@ -1711,9 +1711,9 @@ } }, "node_modules/@ethersproject/providers": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.4.1.tgz", - "integrity": "sha512-p06eiFKz8nu/5Ju0kIX024gzEQIgE5pvvGrBCngpyVjpuLtUIWT3097Agw4mTn9/dEA0FMcfByzFqacBMSgCVg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.4.5.tgz", + "integrity": "sha512-1GkrvkiAw3Fj28cwi1Sqm8ED1RtERtpdXmRfwIBGmqBSN5MoeRUHuwHPppMtbPayPgpFcvD7/Gdc9doO5fGYgw==", "funding": [ { "type": "individual", @@ -8119,9 +8119,9 @@ } }, "node_modules/ethers": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.4.1.tgz", - "integrity": "sha512-SrcddMdCgP1hukDvCPd87Aipbf4NWjQvdfAbZ65XSZGbfyuYPtIrUJPDH5B1SBRsdlfiEgX3eoz28DdBDzMNFg==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.4.6.tgz", + "integrity": "sha512-F7LXARyB/Px3AQC6/QKedWZ8eqCkgOLORqL4B/F0Mag/K+qJSFGqsR36EaOZ6fKg3ZonI+pdbhb4A8Knt/43jQ==", "funding": [ { "type": "individual", @@ -8133,25 +8133,25 @@ } ], "dependencies": { - "@ethersproject/abi": "5.4.0", - "@ethersproject/abstract-provider": "5.4.0", - "@ethersproject/abstract-signer": "5.4.0", + "@ethersproject/abi": "5.4.1", + "@ethersproject/abstract-provider": "5.4.1", + "@ethersproject/abstract-signer": "5.4.1", "@ethersproject/address": "5.4.0", "@ethersproject/base64": "5.4.0", "@ethersproject/basex": "5.4.0", - "@ethersproject/bignumber": "5.4.0", + "@ethersproject/bignumber": "5.4.1", "@ethersproject/bytes": "5.4.0", "@ethersproject/constants": "5.4.0", - "@ethersproject/contracts": "5.4.0", + "@ethersproject/contracts": "5.4.1", "@ethersproject/hash": "5.4.0", "@ethersproject/hdnode": "5.4.0", "@ethersproject/json-wallets": "5.4.0", "@ethersproject/keccak256": "5.4.0", - "@ethersproject/logger": "5.4.0", - "@ethersproject/networks": "5.4.1", + "@ethersproject/logger": "5.4.1", + "@ethersproject/networks": "5.4.2", "@ethersproject/pbkdf2": "5.4.0", - "@ethersproject/properties": "5.4.0", - "@ethersproject/providers": "5.4.1", + "@ethersproject/properties": "5.4.1", + "@ethersproject/providers": "5.4.5", "@ethersproject/random": "5.4.0", "@ethersproject/rlp": "5.4.0", "@ethersproject/sha2": "5.4.0", @@ -8165,48 +8165,6 @@ "@ethersproject/wordlists": "5.4.0" } }, - "node_modules/ethers/node_modules/@ethersproject/abstract-signer": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz", - "integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/abstract-provider": "^5.4.0", - "@ethersproject/bignumber": "^5.4.0", - "@ethersproject/bytes": "^5.4.0", - "@ethersproject/logger": "^5.4.0", - "@ethersproject/properties": "^5.4.0" - } - }, - "node_modules/ethers/node_modules/@ethersproject/bignumber": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz", - "integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==", - "funding": [ - { - "type": "individual", - "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@ethersproject/bytes": "^5.4.0", - "@ethersproject/logger": "^5.4.0", - "bn.js": "^4.11.9" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "license": "MIT" @@ -14675,9 +14633,9 @@ } }, "node_modules/react-router-dom": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz", - "integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", + "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -16073,9 +16031,9 @@ } }, "node_modules/serve": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/serve/-/serve-12.0.0.tgz", - "integrity": "sha512-BkTsETQYynAZ7rXX414kg4X6EvuZQS3UVs1NY0VQYdRHSTYWPYcH38nnDh48D0x6ONuislgjag8uKlU2gTBImA==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/serve/-/serve-12.0.1.tgz", + "integrity": "sha512-CQ4ikLpxg/wmNM7yivulpS6fhjRiFG6OjmP8ty3/c1SBnSk23fpKmLAV4HboTA2KrZhkUPlDfjDhnRmAjQ5Phw==", "dependencies": { "@zeit/schemas": "2.6.0", "ajv": "6.12.6", @@ -18242,9 +18200,9 @@ } }, "node_modules/typescript": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20567,9 +20525,9 @@ } }, "@craco/craco": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.2.0.tgz", - "integrity": "sha512-kLc4GSdgR9D5JiZmSxtzbvBKcUFSJqMXImRjjYf5pacwiyAs3XfQwai7T+pExfLQNUnytgkL8jRFUJeYrkVr7g==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.3.0.tgz", + "integrity": "sha512-SCnfEQxT/6NAbU/3sIWw7gQXtzjjiTp/EZFdJTd8inPURILIy0YajrC2p8qBG2KhFo5cwgOrEDyaGyAFvvuyuA==", "requires": { "cross-spawn": "^7.0.0", "lodash": "^4.17.15", @@ -20617,9 +20575,9 @@ } }, "@ethersproject/abi": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.4.0.tgz", - "integrity": "sha512-9gU2H+/yK1j2eVMdzm6xvHSnMxk8waIHQGYCZg5uvAyH0rsAzxkModzBSpbAkAuhKFEovC2S9hM4nPuLym8IZw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.4.1.tgz", + "integrity": "sha512-9mhbjUk76BiSluiiW4BaYyI58KSbDMMQpCLdsAR+RsT2GyATiNYxVv+pGWRrekmsIdY3I+hOqsYQSTkc8L/mcg==", "requires": { "@ethersproject/address": "^5.4.0", "@ethersproject/bignumber": "^5.4.0", @@ -20633,9 +20591,9 @@ } }, "@ethersproject/abstract-provider": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.4.0.tgz", - "integrity": "sha512-vPBR7HKUBY0lpdllIn7tLIzNN7DrVnhCLKSzY0l8WAwxz686m/aL7ASDzrVxV93GJtIub6N2t4dfZ29CkPOxgA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.4.1.tgz", + "integrity": "sha512-3EedfKI3LVpjSKgAxoUaI+gB27frKsxzm+r21w9G60Ugk+3wVLQwhi1LsEJAKNV7WoZc8CIpNrATlL1QFABjtQ==", "requires": { "@ethersproject/bignumber": "^5.4.0", "@ethersproject/bytes": "^5.4.0", @@ -20714,9 +20672,9 @@ } }, "@ethersproject/contracts": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.4.0.tgz", - "integrity": "sha512-hkO3L3IhS1Z3ZtHtaAG/T87nQ7KiPV+/qnvutag35I0IkiQ8G3ZpCQ9NNOpSCzn4pWSW4CfzmtE02FcqnLI+hw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.4.1.tgz", + "integrity": "sha512-m+z2ZgPy4pyR15Je//dUaymRUZq5MtDajF6GwFbGAVmKz/RF+DNIPwF0k5qEcL3wPGVqUjFg2/krlCRVTU4T5w==", "requires": { "@ethersproject/abi": "^5.4.0", "@ethersproject/abstract-provider": "^5.4.0", @@ -20794,14 +20752,14 @@ } }, "@ethersproject/logger": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.4.0.tgz", - "integrity": "sha512-xYdWGGQ9P2cxBayt64d8LC8aPFJk6yWCawQi/4eJ4+oJdMMjEBMrIcIMZ9AxhwpPVmnBPrsB10PcXGmGAqgUEQ==" + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.4.1.tgz", + "integrity": "sha512-DZ+bRinnYLPw1yAC64oRl0QyVZj43QeHIhVKfD/+YwSz4wsv1pfwb5SOFjz+r710YEWzU6LrhuSjpSO+6PeE4A==" }, "@ethersproject/networks": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.4.1.tgz", - "integrity": "sha512-8SvowCKz9Uf4xC5DTKI8+il8lWqOr78kmiqAVLYT9lzB8aSmJHQMD1GSuJI0CW4hMAnzocpGpZLgiMdzsNSPig==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.4.2.tgz", + "integrity": "sha512-eekOhvJyBnuibfJnhtK46b8HimBc5+4gqpvd1/H9LEl7Q7/qhsIhM81dI9Fcnjpk3jB1aTy6bj0hz3cifhNeYw==", "requires": { "@ethersproject/logger": "^5.4.0" } @@ -20816,17 +20774,17 @@ } }, "@ethersproject/properties": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.4.0.tgz", - "integrity": "sha512-7jczalGVRAJ+XSRvNA6D5sAwT4gavLq3OXPuV/74o3Rd2wuzSL035IMpIMgei4CYyBdialJMrTqkOnzccLHn4A==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.4.1.tgz", + "integrity": "sha512-cyCGlF8wWlIZyizsj2PpbJ9I7rIlUAfnHYwy/T90pdkSn/NFTa5YWZx2wTJBe9V7dD65dcrrEMisCRUJiq6n3w==", "requires": { "@ethersproject/logger": "^5.4.0" } }, "@ethersproject/providers": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.4.1.tgz", - "integrity": "sha512-p06eiFKz8nu/5Ju0kIX024gzEQIgE5pvvGrBCngpyVjpuLtUIWT3097Agw4mTn9/dEA0FMcfByzFqacBMSgCVg==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.4.5.tgz", + "integrity": "sha512-1GkrvkiAw3Fj28cwi1Sqm8ED1RtERtpdXmRfwIBGmqBSN5MoeRUHuwHPppMtbPayPgpFcvD7/Gdc9doO5fGYgw==", "requires": { "@ethersproject/abstract-provider": "^5.4.0", "@ethersproject/abstract-signer": "^5.4.0", @@ -25120,29 +25078,29 @@ "version": "1.8.1" }, "ethers": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.4.1.tgz", - "integrity": "sha512-SrcddMdCgP1hukDvCPd87Aipbf4NWjQvdfAbZ65XSZGbfyuYPtIrUJPDH5B1SBRsdlfiEgX3eoz28DdBDzMNFg==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.4.6.tgz", + "integrity": "sha512-F7LXARyB/Px3AQC6/QKedWZ8eqCkgOLORqL4B/F0Mag/K+qJSFGqsR36EaOZ6fKg3ZonI+pdbhb4A8Knt/43jQ==", "requires": { - "@ethersproject/abi": "5.4.0", - "@ethersproject/abstract-provider": "5.4.0", - "@ethersproject/abstract-signer": "5.4.0", + "@ethersproject/abi": "5.4.1", + "@ethersproject/abstract-provider": "5.4.1", + "@ethersproject/abstract-signer": "5.4.1", "@ethersproject/address": "5.4.0", "@ethersproject/base64": "5.4.0", "@ethersproject/basex": "5.4.0", - "@ethersproject/bignumber": "5.4.0", + "@ethersproject/bignumber": "5.4.1", "@ethersproject/bytes": "5.4.0", "@ethersproject/constants": "5.4.0", - "@ethersproject/contracts": "5.4.0", + "@ethersproject/contracts": "5.4.1", "@ethersproject/hash": "5.4.0", "@ethersproject/hdnode": "5.4.0", "@ethersproject/json-wallets": "5.4.0", "@ethersproject/keccak256": "5.4.0", - "@ethersproject/logger": "5.4.0", - "@ethersproject/networks": "5.4.1", + "@ethersproject/logger": "5.4.1", + "@ethersproject/networks": "5.4.2", "@ethersproject/pbkdf2": "5.4.0", - "@ethersproject/properties": "5.4.0", - "@ethersproject/providers": "5.4.1", + "@ethersproject/properties": "5.4.1", + "@ethersproject/providers": "5.4.5", "@ethersproject/random": "5.4.0", "@ethersproject/rlp": "5.4.0", "@ethersproject/sha2": "5.4.0", @@ -25154,30 +25112,6 @@ "@ethersproject/wallet": "5.4.0", "@ethersproject/web": "5.4.0", "@ethersproject/wordlists": "5.4.0" - }, - "dependencies": { - "@ethersproject/abstract-signer": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz", - "integrity": "sha512-AieQAzt05HJZS2bMofpuxMEp81AHufA5D6M4ScKwtolj041nrfIbIi8ciNW7+F59VYxXq+V4c3d568Q6l2m8ew==", - "requires": { - "@ethersproject/abstract-provider": "^5.4.0", - "@ethersproject/bignumber": "^5.4.0", - "@ethersproject/bytes": "^5.4.0", - "@ethersproject/logger": "^5.4.0", - "@ethersproject/properties": "^5.4.0" - } - }, - "@ethersproject/bignumber": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.4.0.tgz", - "integrity": "sha512-OXUu9f9hO3vGRIPxU40cignXZVaYyfx6j9NNMjebKdnaCL3anCLSSy8/b8d03vY6dh7duCC0kW72GEC4tZer2w==", - "requires": { - "@ethersproject/bytes": "^5.4.0", - "@ethersproject/logger": "^5.4.0", - "bn.js": "^4.11.9" - } - } } }, "eventemitter3": { @@ -29515,9 +29449,9 @@ } }, "react-router-dom": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.1.tgz", - "integrity": "sha512-xhFFkBGVcIVPbWM2KEYzED+nuHQPmulVa7sqIs3ESxzYd1pYg8N8rxPnQ4T2o1zu/2QeDUWcaqST131SO1LR3w==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz", + "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==", "requires": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -30436,9 +30370,9 @@ } }, "serve": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/serve/-/serve-12.0.0.tgz", - "integrity": "sha512-BkTsETQYynAZ7rXX414kg4X6EvuZQS3UVs1NY0VQYdRHSTYWPYcH38nnDh48D0x6ONuislgjag8uKlU2gTBImA==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/serve/-/serve-12.0.1.tgz", + "integrity": "sha512-CQ4ikLpxg/wmNM7yivulpS6fhjRiFG6OjmP8ty3/c1SBnSk23fpKmLAV4HboTA2KrZhkUPlDfjDhnRmAjQ5Phw==", "requires": { "@zeit/schemas": "2.6.0", "ajv": "6.12.6", @@ -31974,9 +31908,9 @@ } }, "typescript": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", - "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==" + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", + "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==" }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4" @@ -33010,4 +32944,4 @@ "version": "0.1.0" } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index d2925fe..67637e3 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dependencies": { "@blackbox-vision/react-qr-reader": "^5.0.0", "@chainlink/contracts": "^0.2.1", - "@craco/craco": "^6.2.0", + "@craco/craco": "^6.3.0", "@fontsource/fira-code": "^4.5.1", "@fontsource/roboto": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0", @@ -29,7 +29,7 @@ "@types/react-router-dom": "^5.1.8", "@types/react-syntax-highlighter": "^13.5.2", "chart.js": "^3.5.1", - "ethers": "^5.4.1", + "ethers": "^5.4.6", "highlightjs-solidity": "^1.2.0", "query-string": "^7.0.1", "react": "^17.0.2", @@ -38,11 +38,11 @@ "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", "react-image": "^4.0.3", - "react-router-dom": "^5.2.1", + "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", "react-syntax-highlighter": "^15.4.4", - "serve": "^12.0.0", - "typescript": "^4.4.2", + "serve": "^12.0.1", + "typescript": "^4.4.3", "use-keyboard-shortcut": "^1.0.6", "web-vitals": "^1.0.1" }, diff --git a/src/AddressTransactions.tsx b/src/AddressTransactions.tsx index 45a07ca..e3a23a0 100644 --- a/src/AddressTransactions.tsx +++ b/src/AddressTransactions.tsx @@ -31,8 +31,8 @@ import { useENSCache } from "./useReverseCache"; import { useFeeToggler } from "./search/useFeeToggler"; import { SelectionContext, useSelection } from "./useSelection"; import { useMultipleETHUSDOracle } from "./usePriceOracle"; +import { useAppConfigContext } from "./useAppConfig"; import { useSourcify } from "./useSourcify"; -import { SourcifySource } from "./url"; type BlockParams = { addressOrName: string; @@ -180,9 +180,7 @@ const AddressTransactions: React.FC = () => { const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const selectionCtx = useSelection(); - const [sourcifySource, setSourcifySource] = useState( - SourcifySource.IPFS_IPNS - ); + const { sourcifySource } = useAppConfigContext(); const rawMetadata = useSourcify( checksummedAddress, provider?.network.chainId, @@ -316,8 +314,6 @@ const AddressTransactions: React.FC = () => { diff --git a/src/App.tsx b/src/App.tsx index 4f1333d..648c58f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { Suspense } from "react"; +import React, { Suspense, useMemo, useState } from "react"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import WarningHeader from "./WarningHeader"; import Home from "./Home"; @@ -9,6 +9,8 @@ import London from "./special/london/London"; import Footer from "./Footer"; import { ConnectionStatus } from "./types"; import { RuntimeContext, useRuntime } from "./useRuntime"; +import { AppConfig, AppConfigContext } from "./useAppConfig"; +import { SourcifySource } from "./url"; const Block = React.lazy(() => import("./Block")); const BlockTransactions = React.lazy(() => import("./BlockTransactions")); @@ -17,6 +19,15 @@ const Transaction = React.lazy(() => import("./Transaction")); const App = () => { const runtime = useRuntime(); + const [sourcifySource, setSourcifySource] = useState( + SourcifySource.IPFS_IPNS + ); + const appConfig = useMemo((): AppConfig => { + return { + sourcifySource, + setSourcifySource, + }; + }, [sourcifySource, setSourcifySource]); return ( LOADING}> @@ -41,21 +52,23 @@ const App = () => { -
- - <Route path="/block/:blockNumberOrHash" exact> - <Block /> - </Route> - <Route path="/block/:blockNumber/txs" exact> - <BlockTransactions /> - </Route> - <Route path="/tx/:txhash"> - <Transaction /> - </Route> - <Route path="/address/:addressOrName/:direction?"> - <AddressTransactions /> - </Route> - </div> + <AppConfigContext.Provider value={appConfig}> + <div className="mb-auto"> + <Title /> + <Route path="/block/:blockNumberOrHash" exact> + <Block /> + </Route> + <Route path="/block/:blockNumber/txs" exact> + <BlockTransactions /> + </Route> + <Route path="/tx/:txhash"> + <Transaction /> + </Route> + <Route path="/address/:addressOrName/:direction?"> + <AddressTransactions /> + </Route> + </div> + </AppConfigContext.Provider> </Route> </Switch> </Router> diff --git a/src/SourcifyMenu.tsx b/src/SourcifyMenu.tsx new file mode 100644 index 0000000..36f43a5 --- /dev/null +++ b/src/SourcifyMenu.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { Menu } from "@headlessui/react"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBars } from "@fortawesome/free-solid-svg-icons/faBars"; +import { SourcifySource } from "./url"; +import { useAppConfigContext } from "./useAppConfig"; + +const SourcifyMenu: React.FC = () => { + const { sourcifySource, setSourcifySource } = useAppConfigContext(); + + return ( + <Menu> + <div className="relative self-stretch"> + <Menu.Button className="w-full h-full flex justify-center items-center space-x-2 text-sm border rounded px-2 py-1"> + <FontAwesomeIcon icon={faBars} size="1x" /> + </Menu.Button> + <Menu.Items className="absolute right-0 mt-1 border p-1 rounded-b bg-white flex flex-col text-sm min-w-max"> + <div className="px-2 py-1 text-xs border-b border-gray-300"> + Sourcify Datasource + </div> + <SourcifyMenuItem + checked={sourcifySource === SourcifySource.IPFS_IPNS} + onClick={() => setSourcifySource(SourcifySource.IPFS_IPNS)} + > + Resolve IPNS + </SourcifyMenuItem> + <SourcifyMenuItem + checked={sourcifySource === SourcifySource.CENTRAL_SERVER} + onClick={() => setSourcifySource(SourcifySource.CENTRAL_SERVER)} + > + Sourcify Servers + </SourcifyMenuItem> + <SourcifyMenuItem + checked={sourcifySource === SourcifySource.CUSTOM_SNAPSHOT_SERVER} + onClick={() => + setSourcifySource(SourcifySource.CUSTOM_SNAPSHOT_SERVER) + } + > + Local Snapshot + </SourcifyMenuItem> + </Menu.Items> + </div> + </Menu> + ); +}; + +type SourcifyMenuItemProps = { + checked?: boolean; + onClick: () => void; +}; + +const SourcifyMenuItem: React.FC<SourcifyMenuItemProps> = ({ + checked = false, + onClick, + children, +}) => ( + <Menu.Item> + {({ active }) => ( + <button + className={`text-sm text-left px-2 py-1 ${ + active ? "border-orange-200 text-gray-500" : "text-gray-400" + } transition-transform transition-colors duration-75 ${ + checked ? "text-gray-900" : "" + }`} + onClick={onClick} + > + {children} + </button> + )} + </Menu.Item> +); + +export default React.memo(SourcifyMenu); diff --git a/src/Title.tsx b/src/Title.tsx index 25b408d..e4c5f47 100644 --- a/src/Title.tsx +++ b/src/Title.tsx @@ -5,6 +5,7 @@ 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 SourcifyMenu from "./SourcifyMenu"; import { RuntimeContext } from "./useRuntime"; const Title: React.FC = () => { @@ -82,6 +83,7 @@ const Title: React.FC = () => { Search </button> </form> + <SourcifyMenu /> </div> </div> </> diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index a093b7f..c9f40a2 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -18,6 +18,7 @@ type TokenTransferItemProps = { tokenMetas: TokenMetas; }; +// TODO: handle partial const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ t, txData, diff --git a/src/Transaction.tsx b/src/Transaction.tsx index e204633..29c7208 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -11,6 +11,8 @@ import { RuntimeContext } from "./useRuntime"; import { SelectionContext, useSelection } from "./useSelection"; import { useInternalOperations, useTxData } from "./useErigonHooks"; import { useETHUSDOracle } from "./usePriceOracle"; +import { useAppConfigContext } from "./useAppConfig"; +import { useSourcify, useTransactionDescription } from "./useSourcify"; type TransactionParams = { txhash: string; @@ -44,6 +46,14 @@ const Transaction: React.FC = () => { txData?.confirmedData?.blockNumber ); + const { sourcifySource } = useAppConfigContext(); + const metadata = useSourcify( + txData?.to, + provider?.network.chainId, + sourcifySource + ); + const txDesc = useTransactionDescription(metadata, txData); + return ( <StandardFrame> <StandardSubtitle>Transaction Details</StandardSubtitle> @@ -71,13 +81,14 @@ const Transaction: React.FC = () => { <Route path="/tx/:txhash/" exact> <Details txData={txData} + txDesc={txDesc} internalOps={internalOps} sendsEthToMiner={sendsEthToMiner} ethUSDPrice={blockETHUSDPrice} /> </Route> <Route path="/tx/:txhash/logs/" exact> - <Logs txData={txData} /> + <Logs txData={txData} metadata={metadata} /> </Route> </Switch> </SelectionContext.Provider> diff --git a/src/address/Contract.tsx b/src/address/Contract.tsx index eece5fd..7ddfe56 100644 --- a/src/address/Contract.tsx +++ b/src/address/Contract.tsx @@ -3,9 +3,9 @@ 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"; +import { useAppConfigContext } from "../useAppConfig"; hljsDefineSolidity(hljs); type ContractProps = { @@ -13,7 +13,6 @@ type ContractProps = { networkId: number; filename: string; source: any; - sourcifySource: SourcifySource; }; const Contract: React.FC<ContractProps> = ({ @@ -21,8 +20,8 @@ const Contract: React.FC<ContractProps> = ({ networkId, filename, source, - sourcifySource, }) => { + const { sourcifySource } = useAppConfigContext(); const content = useContract( checksummedAddress, networkId, diff --git a/src/address/Contracts.tsx b/src/address/Contracts.tsx index 4d322c3..0745d73 100644 --- a/src/address/Contracts.tsx +++ b/src/address/Contracts.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useContext, Fragment } from "react"; import { commify } from "@ethersproject/units"; -import { Menu, RadioGroup } from "@headlessui/react"; +import { Menu } from "@headlessui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; import ContentFrame from "../ContentFrame"; @@ -11,21 +11,16 @@ 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"; +import { openInRemixURL } from "../url"; 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); @@ -39,21 +34,6 @@ const Contracts: React.FC<ContractsProps> = ({ 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"> @@ -145,7 +125,6 @@ const Contracts: React.FC<ContractsProps> = ({ networkId={provider!.network.chainId} filename={selected} source={rawMetadata.sources[selected]} - sourcifySource={sourcifySource} /> )} </div> diff --git a/src/address/RadioButton.tsx b/src/address/RadioButton.tsx deleted file mode 100644 index e93fad3..0000000 --- a/src/address/RadioButton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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; diff --git a/src/components/ModeTab.tsx b/src/components/ModeTab.tsx new file mode 100644 index 0000000..14a33fd --- /dev/null +++ b/src/components/ModeTab.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Tab } from "@headlessui/react"; + +const ModeTab: React.FC = ({ children }) => ( + <Tab + className={({ selected }) => + `border rounded-lg px-2 py-1 bg-gray-100 hover:bg-gray-200 hover:shadow text-xs text-gray-500 hover:text-gray-600 ${ + selected ? "border-blue-300" : "" + }` + } + > + {children} + </Tab> +); + +export default ModeTab; diff --git a/src/components/NavTab.tsx b/src/components/NavTab.tsx index 83cf6d8..b8ff811 100644 --- a/src/components/NavTab.tsx +++ b/src/components/NavTab.tsx @@ -6,6 +6,8 @@ type NavTabProps = { href: string; }; +// TODO: migrate activeClassName because of: https://github.com/remix-run/react-router/releases/tag/v5.3.0 +// TODO: @types/react-router-dom still doesn't support function in className const NavTab: React.FC<NavTabProps> = ({ href, children }) => ( <Tab as={Fragment}> <NavLink diff --git a/src/transaction/DecodedLogSignature.tsx b/src/transaction/DecodedLogSignature.tsx new file mode 100644 index 0000000..42cfbe9 --- /dev/null +++ b/src/transaction/DecodedLogSignature.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { EventFragment } from "@ethersproject/abi"; + +type DecodedLogSignatureProps = { + event: EventFragment; +}; + +const DecodedLogSignature: React.FC<DecodedLogSignatureProps> = ({ event }) => { + return ( + <span> + <span className="text-blue-900 font-bold">{event.name}</span>( + {event.inputs.map((input, i) => ( + <span key={i}> + {i > 0 ? ", " : ""} + <span>{input.format("full")}</span> + </span> + ))} + ){event.anonymous ? " anonymous" : ""} + </span> + ); +}; + +export default React.memo(DecodedLogSignature); diff --git a/src/transaction/DecodedParamRow.tsx b/src/transaction/DecodedParamRow.tsx new file mode 100644 index 0000000..9d9bcf8 --- /dev/null +++ b/src/transaction/DecodedParamRow.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import AddressHighlighter from "../components/AddressHighlighter"; +import DecoratedAddressLink from "../components/DecoratedAddressLink"; +import Copy from "../components/Copy"; +import { ParamType } from "@ethersproject/abi"; +import { TransactionData } from "../types"; + +type DecodedParamRowProps = { + prefix?: string; + i?: number | undefined; + r: any; + paramType: ParamType; + txData: TransactionData; +}; + +const DecodedParamRow: React.FC<DecodedParamRowProps> = ({ + prefix, + i, + r, + paramType, + txData, +}) => { + return ( + <> + <tr className="grid grid-cols-12 gap-x-2 py-2 hover:bg-gray-100"> + <td className="col-span-3 pl-1"> + {prefix && <span className="text-gray-300">{prefix}</span>} + {paramType.name}{" "} + {i !== undefined && ( + <span className="text-gray-400 text-xs">({i})</span> + )} + </td> + <td className="col-span-1 text-gray-500">{paramType.type}</td> + <td className="col-span-8 pr-1 font-code break-all"> + {paramType.baseType === "address" ? ( + <div className="flex items-baseline space-x-2 -ml-1 mr-3"> + <AddressHighlighter address={r.toString()}> + <DecoratedAddressLink + address={r.toString()} + miner={r.toString() === txData.confirmedData?.miner} + txFrom={r.toString() === txData.from} + txTo={r.toString() === txData.to} + /> + </AddressHighlighter> + <Copy value={r.toString()} /> + </div> + ) : paramType.baseType === "bool" ? ( + <span className={`${r ? "text-green-700" : "text-red-700"}`}> + {r.toString()} + </span> + ) : paramType.baseType === "bytes" ? ( + <span> + {r.toString()}{" "} + <span className="font-sans text-xs text-gray-400"> + {r.toString().length / 2 - 1}{" "} + {r.toString().length / 2 - 1 === 1 ? "byte" : "bytes"} + </span> + </span> + ) : paramType.baseType === "tuple" ? ( + <></> + ) : ( + r.toString() + )} + </td> + </tr> + {paramType.baseType === "tuple" && + r.map((e: any, idx: number) => ( + <DecodedParamRow key={idx} + prefix={paramType.name + "."} + r={e} + paramType={paramType.components[idx]} + txData={txData} + /> + ))} + </> + ); +}; + +export default React.memo(DecodedParamRow); diff --git a/src/transaction/DecodedParamsTable.tsx b/src/transaction/DecodedParamsTable.tsx new file mode 100644 index 0000000..ff439d8 --- /dev/null +++ b/src/transaction/DecodedParamsTable.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { ParamType, Result } from "@ethersproject/abi"; +import DecodedParamRow from "./DecodedParamRow"; +import { TransactionData } from "../types"; + +type DecodedParamsTableProps = { + args: Result; + paramTypes: ParamType[]; + txData: TransactionData; +}; + +const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({ + args, + paramTypes, + txData, +}) => ( + <table className="border rounded w-full"> + <thead> + <tr className="grid grid-cols-12 text-left gap-x-2 py-2 bg-gray-100"> + <th className="col-span-3 pl-1"> + name <span className="text-gray-400 text-xs">(index)</span> + </th> + <th className="col-span-1">type</th> + <th className="col-span-8 pr-1">value</th> + </tr> + </thead> + <tbody className="divide-y"> + {args.map((r, i) => ( + <DecodedParamRow + key={i} + i={i} + r={r} + paramType={paramTypes[i]} + txData={txData} + /> + ))} + </tbody> + </table> +); + +export default React.memo(DecodedParamsTable); diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index e5ba19a..bea1587 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -1,6 +1,8 @@ -import React, { useMemo, useState } from "react"; +import React, { useMemo } from "react"; +import { TransactionDescription } from "@ethersproject/abi"; import { BigNumber } from "@ethersproject/bignumber"; import { toUtf8String } from "@ethersproject/strings"; +import { Tab } from "@headlessui/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle"; import { faCube } from "@fortawesome/free-solid-svg-icons/faCube"; @@ -28,9 +30,12 @@ import PercentageBar from "../components/PercentageBar"; import ExternalLink from "../components/ExternalLink"; import RelativePosition from "../components/RelativePosition"; import PercentagePosition from "../components/PercentagePosition"; +import ModeTab from "../components/ModeTab"; +import DecodedParamsTable from "./DecodedParamsTable"; type DetailsProps = { txData: TransactionData; + txDesc: TransactionDescription | null | undefined; internalOps?: InternalOperation[]; sendsEthToMiner: boolean; ethUSDPrice: BigNumber | undefined; @@ -38,6 +43,7 @@ type DetailsProps = { const Details: React.FC<DetailsProps> = ({ txData, + txDesc, internalOps, sendsEthToMiner, ethUSDPrice, @@ -45,7 +51,6 @@ const Details: React.FC<DetailsProps> = ({ const hasEIP1559 = txData.confirmedData?.blockBaseFeePerGas !== undefined && txData.confirmedData?.blockBaseFeePerGas !== null; - const [inputMode, setInputMode] = useState<number>(0); const utfInput = useMemo(() => { try { @@ -309,31 +314,42 @@ const Details: React.FC<DetailsProps> = ({ </> )} <InfoRow title="Input Data"> - <div className="space-y-1"> - <div className="flex space-x-1"> - <button - className={`border rounded-lg px-2 py-1 bg-gray-100 hover:bg-gray-200 hover:shadow text-xs text-gray-500 hover:text-gray-600 ${ - inputMode === 0 ? "border-blue-300" : "" - }`} - onClick={() => setInputMode(0)} - > - Raw - </button> - <button - className={`border rounded-lg px-2 py-1 bg-gray-100 hover:bg-gray-200 hover:shadow text-xs text-gray-500 hover:text-gray-600 ${ - inputMode === 1 ? "border-blue-300" : "" - }`} - onClick={() => setInputMode(1)} - > - UTF-8 - </button> - </div> - <textarea - className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2" - value={inputMode === 0 ? txData.data : utfInput} - readOnly - /> - </div> + <Tab.Group> + <Tab.List className="flex space-x-1 mb-1"> + <ModeTab>Decoded</ModeTab> + <ModeTab>Raw</ModeTab> + <ModeTab>UTF-8</ModeTab> + </Tab.List> + <Tab.Panels> + <Tab.Panel> + {txDesc === undefined ? ( + <>Waiting for data...</> + ) : txDesc === null ? ( + <>No decoded data</> + ) : ( + <DecodedParamsTable + args={txDesc.args} + paramTypes={txDesc.functionFragment.inputs} + txData={txData} + /> + )} + </Tab.Panel> + <Tab.Panel> + <textarea + className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2" + value={txData.data} + readOnly + /> + </Tab.Panel> + <Tab.Panel> + <textarea + className="w-full h-40 bg-gray-50 text-gray-500 font-mono focus:outline-none border rounded p-2" + value={utfInput} + readOnly + /> + </Tab.Panel> + </Tab.Panels> + </Tab.Group> </InfoRow> </ContentFrame> ); diff --git a/src/transaction/LogEntry.tsx b/src/transaction/LogEntry.tsx new file mode 100644 index 0000000..1d4e85f --- /dev/null +++ b/src/transaction/LogEntry.tsx @@ -0,0 +1,116 @@ +import React, { Fragment } from "react"; +import { Log } from "@ethersproject/abstract-provider"; +import { LogDescription } from "@ethersproject/abi"; +import { Tab } from "@headlessui/react"; +import AddressHighlighter from "../components/AddressHighlighter"; +import DecoratedAddressLink from "../components/DecoratedAddressLink"; +import Copy from "../components/Copy"; +import ModeTab from "../components/ModeTab"; +import DecodedParamsTable from "./DecodedParamsTable"; +import DecodedLogSignature from "./DecodedLogSignature"; +import { TransactionData } from "../types"; + +type LogEntryProps = { + txData: TransactionData; + log: Log; + logDesc: LogDescription | null | undefined; +}; + +const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => ( + <div className="flex space-x-10 py-5"> + <div> + <span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500"> + {log.logIndex} + </span> + </div> + <div className="w-full space-y-2"> + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="font-bold text-right">Address</div> + <div className="col-span-11 mr-auto"> + <div className="flex items-baseline space-x-2 -ml-1 mr-3"> + <AddressHighlighter address={log.address}> + <DecoratedAddressLink + address={log.address} + miner={log.address === txData.confirmedData?.miner} + txFrom={log.address === txData.from} + txTo={log.address === txData.to} + /> + </AddressHighlighter> + <Copy value={log.address} /> + </div> + </div> + </div> + <Tab.Group> + <Tab.List className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="text-right">Parameters</div> + <div className="col-span-11 flex space-x-1 mb-1"> + <ModeTab>Decoded</ModeTab> + <ModeTab>Raw</ModeTab> + </div> + </Tab.List> + <Tab.Panels as={Fragment}> + <Tab.Panel className="space-y-2"> + {logDesc === undefined ? ( + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="col-start-2 flex space-x-2 items-center col-span-11"> + Waiting for data... + </div> + </div> + ) : logDesc === null ? ( + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="col-start-2 flex space-x-2 items-center col-span-11"> + No decoded data + </div> + </div> + ) : ( + <> + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="col-start-2 flex space-x-2 items-center col-span-11 font-mono"> + <DecodedLogSignature event={logDesc.eventFragment} /> + </div> + </div> + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="col-start-2 flex space-x-2 items-center col-span-11"> + <DecodedParamsTable + args={logDesc.args} + paramTypes={logDesc.eventFragment.inputs} + txData={txData} + /> + </div> + </div> + </> + )} + </Tab.Panel> + <Tab.Panel className="space-y-2"> + {log.topics.map((t, i) => ( + <div + className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm" + key={i} + > + <div className="text-right">{i === 0 && "Topics"}</div> + <div className="flex space-x-2 items-center col-span-11 font-mono"> + <span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs"> + {i} + </span> + <span>{t}</span> + </div> + </div> + ))} + <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> + <div className="text-right pt-2">Data</div> + <div className="col-span-11"> + <textarea + className="w-full h-40 bg-gray-50 font-mono focus:outline-none border rounded p-2" + value={log.data} + readOnly + /> + </div> + </div> + </Tab.Panel> + </Tab.Panels> + </Tab.Group> + </div> + </div> +); + +export default React.memo(LogEntry); diff --git a/src/transaction/Logs.tsx b/src/transaction/Logs.tsx index e57dc6a..45fe491 100644 --- a/src/transaction/Logs.tsx +++ b/src/transaction/Logs.tsx @@ -1,63 +1,85 @@ -import React from "react"; +import React, { useMemo } from "react"; +import { Interface } from "@ethersproject/abi"; import ContentFrame from "../ContentFrame"; -import DecoratedAddressLink from "../components/DecoratedAddressLink"; +import LogEntry from "./LogEntry"; import { TransactionData } from "../types"; +import { useAppConfigContext } from "../useAppConfig"; +import { Metadata, useMultipleMetadata } from "../useSourcify"; type LogsProps = { txData: TransactionData; + metadata: Metadata | null | undefined; }; -const Logs: React.FC<LogsProps> = ({ txData }) => ( - <ContentFrame tabs> - <div className="text-sm py-4">Transaction Receipt Event Logs</div> - {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"> - {l.logIndex} - </span> - </div> - <div className="w-full space-y-2"> - <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> - <div className="font-bold text-right">Address</div> - <div className="col-span-11 mr-auto"> - <DecoratedAddressLink - address={l.address} - miner={l.address === txData.confirmedData?.miner} - txFrom={l.address === txData.from} - txTo={l.address === txData.to} +const Logs: React.FC<LogsProps> = ({ txData, metadata }) => { + const baseMetadatas = useMemo((): Record<string, Metadata | null> => { + if (!txData.to || metadata === undefined) { + return {}; + } + + const md: Record<string, Metadata | null> = {}; + md[txData.to] = metadata; + return md; + }, [txData.to, metadata]); + + const { sourcifySource } = useAppConfigContext(); + const logAddresses = useMemo( + () => txData.confirmedData?.logs.map((l) => l.address) ?? [], + [txData] + ); + const metadatas = useMultipleMetadata( + baseMetadatas, + logAddresses, + 1, + sourcifySource + ); + const logDescs = useMemo(() => { + if (!txData) { + return undefined; + } + + return txData.confirmedData?.logs.map((l) => { + const mt = metadatas[l.address]; + if (!mt) { + return mt; + } + + const abi = mt.output.abi; + const intf = new Interface(abi as any); + try { + return intf.parseLog({ + topics: l.topics, + data: l.data, + }); + } catch (err) { + console.warn("Couldn't find function signature", err); + return null; + } + }); + }, [metadatas, txData]); + + return ( + <ContentFrame tabs> + {txData.confirmedData && ( + <> + {txData.confirmedData.logs.length > 0 ? ( + <> + {txData.confirmedData.logs.map((l, i) => ( + <LogEntry + key={i} + txData={txData} + log={l} + logDesc={logDescs?.[i]} /> - </div> - </div> - {l.topics.map((t, i) => ( - <div - className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm" - key={i} - > - <div className="text-right">{i === 0 && "Topics"}</div> - <div className="flex space-x-2 items-center col-span-11 font-mono"> - <span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs"> - {i} - </span> - <span>{t}</span> - </div> - </div> - ))} - <div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"> - <div className="text-right pt-2">Data</div> - <div className="col-span-11"> - <textarea - className="w-full h-20 bg-gray-50 font-mono focus:outline-none border rounded p-2" - value={l.data} - readOnly - /> - </div> - </div> - </div> - </div> - ))} - </ContentFrame> -); + ))} + </> + ) : ( + <div className="text-sm py-4">Transaction didn't emit any logs</div> + )} + </> + )} + </ContentFrame> + ); +}; export default React.memo(Logs); diff --git a/src/useAppConfig.ts b/src/useAppConfig.ts new file mode 100644 index 0000000..56007e0 --- /dev/null +++ b/src/useAppConfig.ts @@ -0,0 +1,13 @@ +import React, { useContext } from "react"; +import { SourcifySource } from "./url"; + +export type AppConfig = { + sourcifySource: SourcifySource; + setSourcifySource: (newSourcifySource: SourcifySource) => void; +}; + +export const AppConfigContext = React.createContext<AppConfig>(undefined!); + +export const useAppConfigContext = () => { + return useContext(AppConfigContext); +}; diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index 477f7b4..7322a0d 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -225,16 +225,20 @@ export const useTxData = ( continue; } const erc20Contract = new Contract(t.token, erc20, provider); - const [name, symbol, decimals] = await Promise.all([ - erc20Contract.name(), - erc20Contract.symbol(), - erc20Contract.decimals(), - ]); - tokenMetas[t.token] = { - name, - symbol, - decimals, - }; + try { + const [name, symbol, decimals] = await Promise.all([ + erc20Contract.name(), + erc20Contract.symbol(), + erc20Contract.decimals(), + ]); + tokenMetas[t.token] = { + name, + symbol, + decimals, + }; + } catch (err) { + console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err); + } } setTxData({ diff --git a/src/useSourcify.ts b/src/useSourcify.ts index e8806b9..b60fde9 100644 --- a/src/useSourcify.ts +++ b/src/useSourcify.ts @@ -1,4 +1,6 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; +import { Interface } from "@ethersproject/abi"; +import { TransactionData } from "./types"; import { sourcifyMetadata, SourcifySource, sourcifySourceFile } from "./url"; export type Metadata = { @@ -36,44 +38,114 @@ export type Metadata = { }; }; +export const fetchSourcifyMetadata = async ( + checksummedAddress: string, + chainId: number, + source: SourcifySource, + abortController: AbortController +): Promise<Metadata | null> => { + try { + const contractMetadataURL = sourcifyMetadata( + checksummedAddress, + chainId, + source + ); + const result = await fetch(contractMetadataURL, { + signal: abortController.signal, + }); + if (result.ok) { + const _metadata = await result.json(); + return _metadata; + } + + return null; + } catch (err) { + console.error(err); + return null; + } +}; + export const useSourcify = ( checksummedAddress: string | undefined, chainId: number | undefined, source: SourcifySource -) => { +): Metadata | null | undefined => { const [rawMetadata, setRawMetadata] = useState<Metadata | null | undefined>(); useEffect(() => { if (!checksummedAddress || chainId === undefined) { return; } - setRawMetadata(undefined); + + const abortController = new AbortController(); 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); - } + const _metadata = await fetchSourcifyMetadata( + checksummedAddress, + chainId, + source, + abortController + ); + setRawMetadata(_metadata); }; fetchMetadata(); + + return () => { + abortController.abort(); + }; }, [checksummedAddress, chainId, source]); return rawMetadata; }; +export const useMultipleMetadata = ( + baseMetadatas: Record<string, Metadata | null>, + checksummedAddress: (string | undefined)[], + chainId: number | undefined, + source: SourcifySource +): Record<string, Metadata | null | undefined> => { + const [rawMetadata, setRawMetadata] = useState< + Record<string, Metadata | null | undefined> + >({}); + + useEffect(() => { + if (!checksummedAddress || chainId === undefined) { + return; + } + setRawMetadata({}); + + const abortController = new AbortController(); + const fetchMetadata = async (addresses: string[]) => { + const promises: Promise<Metadata | null>[] = []; + for (const addr of addresses) { + promises.push( + fetchSourcifyMetadata(addr, chainId, source, abortController) + ); + } + + const results = await Promise.all(promises); + const metadatas: Record<string, Metadata | null> = { ...baseMetadatas }; + for (let i = 0; i < results.length; i++) { + metadatas[addresses[i]] = results[i]; + } + setRawMetadata(metadatas); + }; + + const deduped = new Set( + checksummedAddress.filter( + (a): a is string => a !== undefined && baseMetadatas[a] === undefined + ) + ); + fetchMetadata(Array.from(deduped)); + + return () => { + abortController.abort(); + }; + }, [baseMetadatas, checksummedAddress, chainId, source]); + + return rawMetadata; +}; + export const useContract = ( checksummedAddress: string, networkId: number, @@ -88,6 +160,7 @@ export const useContract = ( return; } + const abortController = new AbortController(); const readContent = async () => { const normalizedFilename = filename.replaceAll(/[@:]/g, "_"); const url = sourcifySourceFile( @@ -96,14 +169,46 @@ export const useContract = ( normalizedFilename, sourcifySource ); - const res = await fetch(url); + const res = await fetch(url, { signal: abortController.signal }); if (res.ok) { const _content = await res.text(); setContent(_content); } }; readContent(); + + return () => { + abortController.abort(); + }; }, [checksummedAddress, networkId, filename, source.content, sourcifySource]); return content; }; + +export const useTransactionDescription = ( + metadata: Metadata | null | undefined, + txData: TransactionData | null | undefined +) => { + const txDesc = useMemo(() => { + if (metadata === null) { + return null; + } + if (!metadata || !txData) { + return undefined; + } + + const abi = metadata.output.abi; + const intf = new Interface(abi as any); + try { + return intf.parseTransaction({ + data: txData.data, + value: txData.value, + }); + } catch (err) { + console.warn("Couldn't find function signature", err); + return null; + } + }, [metadata, txData]); + + return txDesc; +};