├── .nvmrc ├── .eslintignore ├── postcss.config.js ├── static └── icons │ ├── 128-disabled.png │ └── 128-enabled.png ├── storelisting └── screenshot.jpeg ├── .stylelintrc.js ├── .babelrc ├── .gitignore ├── src ├── lib │ ├── chrome-helpers.js │ ├── vue-setup.js │ ├── init-filters.js │ ├── storage-helpers.js │ ├── inject-scripts.js │ └── message-passing.js ├── popup │ ├── index.js │ └── popup-page.vue ├── options │ ├── index.js │ └── options-page.vue ├── style │ ├── _mixins.less │ ├── core.less │ ├── _colors.less │ ├── _variables.less │ └── typography.less ├── devtools-panel │ ├── index.js │ ├── router.js │ ├── tabs │ │ ├── contracts-tab.vue │ │ └── logs │ │ │ ├── single-log.vue │ │ │ └── index.vue │ ├── devtools-page.vue │ └── store.js ├── injector.js ├── devtools.js ├── background.js ├── manifest.js └── injected.js ├── README.md ├── config └── env.js ├── .eslintrc.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 13.8.0 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'autoprefixer': {}, 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /static/icons/128-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthDevTools/ethdevtools/HEAD/static/icons/128-disabled.png -------------------------------------------------------------------------------- /static/icons/128-enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthDevTools/ethdevtools/HEAD/static/icons/128-enabled.png -------------------------------------------------------------------------------- /storelisting/screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EthDevTools/ethdevtools/HEAD/storelisting/screenshot.jpeg -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | indentation: 2, 4 | linebreaks: 'unix', 5 | 'length-zero-no-unit': true, 6 | }, 7 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false }], 4 | ], 5 | "plugins": [ 6 | ["@babel/plugin-transform-runtime", {}] 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | config/local.js 3 | config/devkey.pem 4 | node_modules/ 5 | /dist 6 | npm-debug.log* 7 | *.log 8 | /.vscode 9 | design/ 10 | TODO 11 | 12 | -------------------------------------------------------------------------------- /src/lib/chrome-helpers.js: -------------------------------------------------------------------------------- 1 | export function openExtensionOptionsPage() { 2 | if (chrome.runtime.openOptionsPage) chrome.runtime.openOptionsPage(); 3 | else window.open(chrome.runtime.getURL('options.html')); 4 | } 5 | -------------------------------------------------------------------------------- /src/popup/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import '@/lib/vue-setup'; 3 | 4 | import root from './popup-page'; 5 | 6 | Vue.config.productionTip = false; 7 | new Vue({ 8 | el: '#app', 9 | render: (h) => h(root), 10 | }); 11 | -------------------------------------------------------------------------------- /src/lib/vue-setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Meta from 'vue-meta'; 3 | 4 | import './init-filters'; 5 | // import '@/components/register-global-components'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | Vue.use(Meta); 10 | -------------------------------------------------------------------------------- /src/options/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import '@/lib/vue-setup'; 3 | 4 | import root from './options-page'; 5 | 6 | Vue.config.productionTip = false; 7 | new Vue({ 8 | el: '#app', 9 | render: (h) => h(root), 10 | }); 11 | -------------------------------------------------------------------------------- /src/style/_mixins.less: -------------------------------------------------------------------------------- 1 | // AUTO INCLUDED 2 | // DO NOT ADD ANY STYLES - ONLY VARIABLES AND MIXINS! 3 | 4 | .unstyled-list() { 5 | padding: 0; 6 | margin: 0; 7 | li { 8 | list-style: none; 9 | margin-left: 0; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/devtools-panel/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import '@/lib/vue-setup'; 3 | 4 | import root from './devtools-page'; 5 | import router from './router'; 6 | import store from './store'; 7 | 8 | window.store = store; // helpful for debugging 9 | 10 | Vue.config.productionTip = false; 11 | 12 | new Vue({ 13 | el: '#app', 14 | store, 15 | router, 16 | render: (h) => h(root), 17 | }); 18 | -------------------------------------------------------------------------------- /src/style/core.less: -------------------------------------------------------------------------------- 1 | // auto inlcluded from webpack setup 2 | //@import './_colors.less'; 3 | //@import './_variables.less'; 4 | 5 | 6 | @import 'normalize.css'; 7 | @import './typography.less'; 8 | 9 | html, body { 10 | box-sizing: border-box; 11 | height: 100%; 12 | background: #f2f5f7; 13 | } 14 | *, *:before, *:after { 15 | box-sizing: border-box; 16 | } 17 | 18 | ul.unstyled { 19 | .unstyled-list(); 20 | } 21 | 22 | .pad { 23 | padding: 15px; 24 | } 25 | .pad-x { 26 | padding-left: 15px; 27 | padding-right: 15px; 28 | } 29 | .pad-y { 30 | padding-top: 15px; 31 | padding-bottom: 15px; 32 | } 33 | -------------------------------------------------------------------------------- /src/devtools-panel/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | import LogsTab from './tabs/logs'; 5 | import ContractsTab from './tabs/contracts-tab'; 6 | 7 | Vue.use(Router); 8 | 9 | const router = new Router({ 10 | mode: 'hash', 11 | // scrollBehavior(to, from, savedPosition) { 12 | // return savedPosition || { x: 0, y: 0 }; 13 | // }, 14 | routes: [ 15 | { path: '', name: 'home', redirect: { name: 'logs' } }, 16 | { path: '/logs', name: 'logs', component: LogsTab }, 17 | { path: '/contracts', name: 'contracts', component: ContractsTab }, 18 | ], 19 | }); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /src/devtools-panel/tabs/contracts-tab.vue: -------------------------------------------------------------------------------- 1 | 6 | 28 | 30 | -------------------------------------------------------------------------------- /src/popup/popup-page.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | 30 | 36 | -------------------------------------------------------------------------------- /src/lib/init-filters.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import Vue from 'vue'; 4 | 5 | import _ from 'lodash'; 6 | import formatDate from 'date-fns/format'; 7 | 8 | 9 | // return up to 2 decimals 10 | // the "+" will get rid of unnecessary trailing zeros 11 | Vue.filter('percent', (value) => `${+(value * 100).toFixed(2)}%`); 12 | 13 | Vue.filter('friendly-date', (value) => { 14 | if (!value) return '---'; 15 | return formatDate(value, 'MMMM Do, YYYY'); 16 | }); 17 | 18 | Vue.filter('date', (value) => { 19 | if (!value) return '---'; 20 | return formatDate(value, 'YYYY-MM-DD'); 21 | }); 22 | Vue.filter('datetime', (value) => { 23 | if (!value) return '---'; 24 | return formatDate(value, 'YYYY-MM-DD @ h:mma'); 25 | }); 26 | Vue.filter('logtime', (value) => { 27 | if (!value) return '---'; 28 | return formatDate(value, 'HH:mm:ss.SSS'); 29 | }); 30 | 31 | Vue.filter('capitalize', (value) => { 32 | if (!value) return '---'; 33 | return value.charAt(0).toUpperCase() + value.slice(1); 34 | }); 35 | -------------------------------------------------------------------------------- /src/style/_colors.less: -------------------------------------------------------------------------------- 1 | // AUTO INCLUDED 2 | // DO NOT ADD ANY STYLES - ONLY VARIABLES AND MIXINS! 3 | 4 | @black: #111; 5 | @dark-gray: #333; 6 | @blue: #01255A; 7 | @gray-blue: #8D99AE; 8 | @blue-green: #06ABDC; 9 | @navy: #38527d; 10 | @red: #f44242; 11 | 12 | @border-blue: #0140ad; 13 | @cta-blue: #019BC9; 14 | 15 | @bluegray-text: #2c3e50; 16 | @light-blue: #1ee9ff; 17 | 18 | @green: #27ae60; 19 | 20 | // @error-red-bg: #c40013; 21 | // @error-red-border: #AA0000; 22 | // @error-red-text: #990000; 23 | 24 | @error-red-bg: #c40013; 25 | @error-red-border: #a85a5e; 26 | @error-red-text: #871616; 27 | 28 | 29 | 30 | // Partner Brand colors 31 | @airbnb-pink: #fd5c63; 32 | @facebook-blue: #3b5998; 33 | @homeaway-blue: #0067db; 34 | @stripe-blue: #6772e5; 35 | @shopify-green: #50B83C; 36 | @guesty-blue: #0073ad; 37 | @flipkey-orange: #CF772A; 38 | @authorizenet-blue: #1C3141; 39 | @cratejoy-color: #55bec3; 40 | @bigcommerce-blue: #0067db; 41 | @paypal-blue: #253B80; 42 | @amazon-blue: #232f3e; 43 | @braintree-gray: #303336; 44 | @square-gray: #303336; 45 | -------------------------------------------------------------------------------- /src/lib/storage-helpers.js: -------------------------------------------------------------------------------- 1 | // Moved this into one file so that we can easily swap between storing data local or in sync storage 2 | 3 | import chromep from 'chrome-promise'; 4 | 5 | // select if you want to store data locally or in synced storage 6 | // SEE https://developer.chrome.com/extensions/storage 7 | // you can change this setting in config/env.js 8 | const useSyncStorage = process.env.CHROME_STORAGE_ENGINE === 'sync'; 9 | const chromeStorage = useSyncStorage ? chromep.storage.sync : chromep.storage.local; 10 | 11 | // keys can be a single string or array of strings 12 | // returns an object with those keys and the values from chrome storage 13 | export async function getSettings(keys) { 14 | return chromeStorage.get(keys); 15 | } 16 | 17 | // takes a single key and returns the value from storage 18 | export async function getSetting(key) { 19 | const settings = await getSettings(key); 20 | return settings[key]; 21 | } 22 | 23 | // takes a an object of key/values to save in storage 24 | export async function setSettings(settingsToSave) { 25 | return chromeStorage.set(settingsToSave); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/inject-scripts.js: -------------------------------------------------------------------------------- 1 | export function injectFunctionAsScript(fn) { 2 | // this weird injection technique means the code in that function does not have access to 3 | // the outer scope, and we can't use webpack to write a normal file 4 | console.log('INSTALLING SCRIPT DIRECTLY'); 5 | const source = `;(${fn.toString()})(window)`; 6 | 7 | const script = document.createElement('script'); 8 | script.textContent = source; 9 | document.documentElement.appendChild(script); 10 | script.parentNode.removeChild(script); 11 | } 12 | 13 | 14 | export function injectScriptFile(fileName) { 15 | // so instead we can inject a script tag with an external file to be loaded 16 | // but this one is not guaranteed to execute first :( 17 | 18 | const injectedScript = document.createElement('script'); 19 | // injected script must be added to web_accessible_resources in manifest.js 20 | injectedScript.src = chrome.runtime.getURL(fileName); 21 | // injectedScript.onload = function () { this.remove(); }; 22 | // (document.head || document.documentElement).appendChild(injectedScript); 23 | document.documentElement.appendChild(injectedScript); 24 | injectedScript.parentNode.removeChild(injectedScript); 25 | } 26 | -------------------------------------------------------------------------------- /src/injector.js: -------------------------------------------------------------------------------- 1 | // This script is injected into every page (can be configured in manifest) 2 | // it is responsible for injecting more scripts and handling communication back to the extension 3 | 4 | // import _ from 'lodash'; 5 | import { broadcastMessage, initializeWebpageMessageRelayer } from '@/lib/message-passing'; 6 | import { injectFunctionAsScript, injectScriptFile } from '@/lib/inject-scripts'; 7 | 8 | // our injected scripts cannot communicate directly with our extension 9 | // so we have to add a relayer which passes them 10 | initializeWebpageMessageRelayer(); 11 | 12 | // send an initial page loaded message (also fires on page reload) 13 | broadcastMessage({ action: 'page_reload' }); 14 | 15 | async function inlineInjectedScript(win) { 16 | if (win.ethereum) { 17 | console.log('found ethereum from directly injected script'); 18 | } else { 19 | console.log('ethereum NOT found from directly injected script'); 20 | } 21 | } 22 | 23 | if (document instanceof HTMLDocument) { 24 | console.log('INJECTOR SCRIPT!', window, chrome.runtime); 25 | 26 | // injecting the script directly ensures it is executed first 27 | injectFunctionAsScript(inlineInjectedScript); 28 | injectScriptFile('js/injected.js'); 29 | } 30 | -------------------------------------------------------------------------------- /src/options/options-page.vue: -------------------------------------------------------------------------------- 1 | 15 | 59 | 60 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ETH Dev Tools - Chrome Extension 2 | 3 | This is a developer tools extension for Google Chrome. It adds a new "web3" panel that intercepts web3 to: 4 | - log all web3 interactions 5 | - explore contracts that have been loaded on the page 6 | 7 | ## Developing locally 8 | 9 | run `npm run dev` 10 | 11 | - go to `chrome://extensions` 12 | - enable "developer mode" 13 | - click "Load unpacked" and select the `dist` folder of this project 14 | 15 | Live reloading will mostly handle things for you, but if you change the manifest, env vars, or webpack config, you will need to stop and restart `npm run dev`. 16 | 17 | Also some specific errors may require going back to `chrome://extensions` and re-loading the plugin. 18 | 19 | **IMPORTANT:** Make sure the EXTENSION_ID in your env file matches the extension ID that chrome provides when you install the unpacked extension 20 | 21 | ## Building for production 22 | 23 | run `npm run build` 24 | 25 | This will build the repo and zip it up into a zip file in the dist folder that can be uploaded to the chrome app store or shared with others. 26 | 27 | ## Configuration 28 | 29 | Configuration is stored in config/env.js and exposed in `process.env` via `webpack.DefinePlugin`. Defaults are loaded first and then overrides depending on the env being built for. 30 | 31 | Optionally, you can create a `config/local.js` file (not checked into git) with overrides to be loaded only during local development. 32 | 33 | ### Credits 34 | 35 | ETH Dev Tools was originally built as a hackathon project at [ETHDenver 2019](https://www.ethdenver.com/) by: 36 | 37 | - Billy Rennekamp [(github @okwme)](https://github.com/okwme) 38 | - Aidan Musnitsky [(github @musnit)](https://github.com/musnit) 39 | - Theo Ephraim - [(github @theoephraim)](http://github.com/theoephraim) 40 | 41 | A grant from [Consensys](https://consensys.net/) has helped drive further development 42 | 43 | The basic development setup is based on Theo's [vue chrome extension template](https://github.com/theoephraim/vue-chrome-extension-template) -------------------------------------------------------------------------------- /src/devtools.js: -------------------------------------------------------------------------------- 1 | // This script is called when the user opens the Chrome devtools on a page 2 | // and it persists until the devtools panel is closed 3 | // here we intiailize any panels and sidebar panes 4 | 5 | import { broadcastMessage, listenForMessagesFromTab } from '@/lib/message-passing'; 6 | 7 | let panel; 8 | let panelShown = false; 9 | let sidebar; 10 | 11 | const inspectedTabId = chrome.devtools.inspectedWindow.tabId; 12 | const chromeTheme = chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light'; 13 | 14 | function createPanel() { 15 | chrome.devtools.panels.create('Web3', 'icons/128.png', `devtools-panel.html?theme=${chromeTheme}`, (_panel) => { 16 | panel = _panel; 17 | panel.onShown.addListener(() => { 18 | broadcastMessage({ 19 | action: 'devtools_panel_shown', 20 | }); 21 | panelShown = true; 22 | }); 23 | panel.onHidden.addListener(() => { 24 | broadcastMessage({ 25 | action: 'devtools_panel_hidden', 26 | }); 27 | panelShown = false; 28 | }); 29 | }); 30 | } 31 | 32 | // createPanel(); 33 | 34 | const WEB3_ACTIONS = ['enable', 'send']; 35 | 36 | // also listen for messages from our injected script, so that if devtools was already open 37 | // but panel has not yet been initialized, we can initialize it 38 | // NOTE - our background script already filters out messages and only sends relevant ones 39 | listenForMessagesFromTab(inspectedTabId, (payload, sender, reply) => { 40 | if (WEB3_ACTIONS.includes(payload.action) && !panel) createPanel(); 41 | 42 | // const enabled = broadcastMessage({ action: 'check_devtools_enabled' }); 43 | // console.log('received a message... Enabled? ', enabled, panel); 44 | // if (enabled && !panel) createPanel(); 45 | }); 46 | 47 | 48 | // send message to background script to check if we should show devtools panel for this 49 | // for example, if we have detected something on the page 50 | (async function init() { 51 | const enabled = await broadcastMessage({ action: 'check_devtools_enabled' }); 52 | console.log('Enabled? ', enabled, panel); 53 | if (enabled && !panel) createPanel(); 54 | }()); 55 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const chalk = require('chalk'); 3 | 4 | // WARNING - do not include any sensitive keys here 5 | // best practice is to keep this all out of git, but since these are all 6 | // published in the app anyway, we're not at any risk here 7 | 8 | const configsByEnv = { 9 | // defaults are always applied, and then overridden depending on the environment 10 | default: { 11 | NODE_ENV: 'development', 12 | MY_ENV: 'development', 13 | API_TIMEOUT: 30000, 14 | 15 | CHROME_STORAGE_ENGINE: 'local', // can switch to 'sync' - see lib/storage-helpers 16 | EXTENSION_MESSAGE_ID: 'ethdevtools', // to identify messages from our extension - see lib/message-passing 17 | 18 | // the public key is based off a .pem file and will keep the id consistent 19 | EXTENSION_ID: 'lbnmcceiaknbfcpjojdgceglaeaejngf', 20 | EXTENSION_KEY: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkA2Vre4W36i9yVDIMQdE/a7TDEUHaDhShbSto4MyfptCcmejwJoRGk3rxWQQMzEPb23+M5P/ubqlvI/lWhoJKhDbp6jSe63ImdHFIlgUAEf30zLYoEjCyfMZGOh8+A71JchrLNL8PqhOeZn9vY0wqUBGic1co9XSZQ7aMNLcce+LJIEN6+RIyPSxiMafpRZsj/kLM9L2/XkKH7aiXFwDPm9YEuap/JKhVf9YQjfVRUPaUA3rnd73x54/redUTV4sTcAaEyPzM/qyS6mCnAExtPyPZ8IOdQaUwFfHCarobnPqX0nDzzHRCVixXF0H0UUk33H2fvM6N02HCW23iFrqTwIDAQAB' 21 | }, 22 | development: { 23 | }, 24 | test: { 25 | NODE_ENV: 'test', 26 | MY_ENV: 'test', 27 | }, 28 | staging: { 29 | NODE_ENV: 'production', 30 | MY_ENV: 'staging', 31 | 32 | }, 33 | production: { 34 | NODE_ENV: 'production', 35 | MY_ENV: 'production', 36 | }, 37 | }; 38 | 39 | const ENVIRONMENT_VARS = { 40 | ...configsByEnv.default, 41 | ...configsByEnv[process.env.LOAD_ENV || 'development'] 42 | }; 43 | 44 | 45 | // allow some config overrides while working on local dev 46 | // loaded from a local.js file which is git ignored 47 | if (ENVIRONMENT_VARS.NODE_ENV === 'development') { 48 | try { 49 | Object.assign(ENVIRONMENT_VARS, require('./local.js')); 50 | } catch (err) { 51 | // do nothing... 52 | } 53 | } 54 | 55 | console.log(chalk.blue('============ CURRENT CONFIG ============')); 56 | console.log(ENVIRONMENT_VARS); 57 | console.log(chalk.blue('========================================')); 58 | Object.assign(process.env, ENVIRONMENT_VARS); 59 | 60 | module.exports = { 61 | env: ENVIRONMENT_VARS, 62 | publicEnv: _.mapValues(ENVIRONMENT_VARS, (val) => JSON.stringify(val)), 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /src/style/_variables.less: -------------------------------------------------------------------------------- 1 | // AUTO INCLUDED 2 | // DO NOT ADD ANY STYLES - ONLY VARIABLES AND MIXINS! 3 | 4 | // TYPE 5 | @default-font-size: 16px; // Default font-size in px 6 | @default-line-height: 1.5; // Default line height in ems 7 | 8 | @regular-font: Helvetica, Arial, sans-serif; 9 | @fancy-font: Helvetica, Arial, sans-serif; 10 | 11 | @default-line-height-em: unit(@default-line-height, em); 12 | @small-font-size: 14px; 13 | 14 | 15 | // Overall site width for the static pages. 16 | @max-width: 1440px; 17 | @wrapper-width: 1100px; 18 | 19 | // Media Query Breakpoints 20 | // small used to be up to 767 but was increased to accomodate desktop nav 21 | // small is up to 894 22 | @mobile-small: 330px; // mobile small is the width of an iPhone 5 23 | @medium-screen: 768px; // medium is 768-1279 24 | @large-screen: 1280px; // large is 1280 and up 25 | // having issues geting the +1's to work correctly 26 | 27 | // Grid 28 | @num-cols: 12; 29 | @max-row-width-px: 1440px; 30 | @max-row-width: unit(@max-row-width-px); 31 | @col-gutter: 20; // Column gutter, in pixels 32 | @default-float: left; // text direction (ltr = left, rtl = right) 33 | 34 | 35 | @header-bar-height--desktop: 60px; 36 | @header-bar-height--mobile: 50px; 37 | 38 | // AUTOMATIC VARIABLES - DO NOT MODIFY ///////////////////////////////////////// 39 | @medium-screen-minus-1: (@medium-screen - 1); 40 | @large-screen-minus-1: (@large-screen - 1); 41 | 42 | @mq-screen: ~"only screen"; 43 | @mq-small: @mq-screen; 44 | @mq-medium: ~"only screen and (min-width:@{medium-screen})"; 45 | @mq-large: ~"only screen and (min-width:@{large-screen})"; 46 | 47 | @mq-mobile-small-only: ~"only screen and (max-width:@{mobile-small})"; 48 | @mq-small-only: ~"only screen and (max-width:@{medium-screen-minus-1})"; 49 | @mq-medium-only: ~"only screen and (min-width:@{medium-screen}) and (max-width:@{large-screen-minus-1})"; 50 | 51 | @mq-landscape: ~"only screen and (orientation: landscape)"; 52 | @mq-portrait: ~"only screen and (orientation: portrait)"; 53 | 54 | 55 | // Automatic variables 56 | @em: unit(@default-font-size, em); // Shorthand for outputting ems, e.g. "12/@em" 57 | @rem: unit(@default-font-size, rem); // Shorthand for outputting ems, e.g. "12/@em" 58 | @num-decimal-places: 5; 59 | @default-float-opp: if(@default-float = 'left', 'right', 'left'); 60 | 61 | @grid-gutter: @col-gutter * .5 / @rem; 62 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint', 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | globals: { 13 | "chrome": true, 14 | }, 15 | extends: [ 16 | 'airbnb-base', 17 | 'plugin:vue/recommended', 18 | ], 19 | // required to lint *.vue files 20 | plugins: [ 21 | 'html' 22 | ], 23 | // check if imports actually resolve 24 | settings: { 25 | 'import/resolver': { 26 | webpack: { 27 | config: 'build/webpack.base.conf.js' 28 | } 29 | } 30 | }, 31 | // add your custom rules here 32 | rules: { 33 | // don't require .vue extension when importing 34 | 'import/extensions': ['error', 'always', { 35 | js: 'never', 36 | vue: 'never' 37 | }], 38 | // allow optionalDependencies 39 | 'import/no-extraneous-dependencies': ['error', { 40 | optionalDependencies: ['test/unit/index.js'] 41 | }], 42 | // allow debugger during development 43 | 'no-return-assign': 0, 44 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 45 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 46 | 'no-unused-vars': process.env.NODE_ENV === 'production' 47 | ? ['error', { "args": "none" }] 48 | : 'off' 49 | , 50 | 'no-plusplus': 0, // i++ OK :D 51 | 'no-else-return': ["error", { allowElseIf: true }], 52 | 'arrow-parens': ["error", "always"], // Forces `(thing) => thing.x` 53 | 'no-param-reassign': ['error', { 54 | props: true, 55 | ignorePropertyModificationsFor: [ 56 | 'state', // for vuex store 57 | 'payload', 58 | ], 59 | }], 60 | // TODO: write custom rule to allow as object props 61 | 'global-require': 0, // allows importing components into an object 62 | // TODO: figure out how to enforce this just within vue watchers? 63 | 'func-names': 0, 64 | 'import/prefer-default-export': 0, // sometimes makes sense if file will soon be expanded 65 | 'radix': 0, 66 | 'no-confusing-arrow': 0, 67 | 'max-len': 0, 68 | 'no-use-before-define': 0, 69 | 'prefer-rest-params': 0, 70 | 'import/no-named-as-default-member': 0, 71 | 'import/no-named-as-default': 0, 72 | 'no-new': 0, 73 | 'no-underscore-dangle': 0, 74 | 'no-empty': 0 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/devtools-panel/devtools-page.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 27 | 28 | 114 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chrome-extension-template", 3 | "version": "0.0.1", 4 | "description": "Vue Chrome Extension Example", 5 | "private": true, 6 | "scripts": { 7 | "dev": "webpack --config build/webpack.dev.conf.js --hide-modules", 8 | "build": "webpack --config build/webpack.prod.conf.js -p --progress --hide-modules --colors", 9 | "lint": "LOAD_ENV=production eslint --fix --ext .js,.vue src/" 10 | }, 11 | "dependencies": { 12 | "abi-decoder": "^2.3.0", 13 | "chrome-promise": "^3.0.5", 14 | "clean-webpack-plugin": "^3.0.0", 15 | "date-fns": "^2.9.0", 16 | "lodash": "^4.17.15", 17 | "normalize.css": "^8.0.1", 18 | "vue": "^2.6.11", 19 | "vue-awesome": "^4.0.2", 20 | "vue-json-pretty": "^1.6.3", 21 | "vue-meta": "^2.3.2", 22 | "vue-router": "^3.1.5", 23 | "vuelidate": "^0.7.5", 24 | "vuex": "^3.1.2", 25 | "web3-eth-abi": "^1.2.6" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.8.4", 29 | "@babel/plugin-transform-runtime": "^7.8.3", 30 | "@babel/preset-env": "^7.8.4", 31 | "autoprefixer": "^9.7.4", 32 | "babel-eslint": "^10.0.3", 33 | "babel-loader": "^8.0.6", 34 | "chalk": "^3.0.0", 35 | "copy-webpack-plugin": "^5.1.1", 36 | "css-loader": "^3.4.2", 37 | "eslint": "^6.8.0", 38 | "eslint-config-airbnb-base": "^14.0.0", 39 | "eslint-import-resolver-webpack": "^0.12.1", 40 | "eslint-loader": "^3.0.3", 41 | "eslint-plugin-html": "^6.0.0", 42 | "eslint-plugin-import": "^2.20.1", 43 | "eslint-plugin-vue": "^6.2.1", 44 | "file-loader": "^5.1.0", 45 | "friendly-errors-webpack-plugin": "^1.7.0", 46 | "generate-json-from-js-webpack-plugin": "^0.1.1", 47 | "html-webpack-plugin": "^3.2.0", 48 | "html-webpack-template": "^6.2.0", 49 | "husky": "^4.2.3", 50 | "less": "^3.11.1", 51 | "less-loader": "^5.0.0", 52 | "mini-css-extract-plugin": "^0.9.0", 53 | "postcss-loader": "^3.0.0", 54 | "pug": "^2.0.4", 55 | "pug-plain-loader": "^1.0.0", 56 | "sass-resources-loader": "^2.0.1", 57 | "stylelint": "^13.2.1", 58 | "stylelint-webpack-plugin": "^1.2.3", 59 | "vue-loader": "^15.9.0", 60 | "vue-style-loader": "^4.1.2", 61 | "vue-template-compiler": "^2.6.11", 62 | "webpack": "^4.41.6", 63 | "webpack-assets-manifest": "^3.1.1", 64 | "webpack-bundle-analyzer": "^3.6.0", 65 | "webpack-cli": "^3.3.11", 66 | "webpack-extension-reloader": "^1.1.4", 67 | "webpack-merge": "^4.2.2", 68 | "zip-webpack-plugin": "^3.0.0" 69 | }, 70 | "engines": { 71 | "node": "^13.8.0", 72 | "npm": "^6.13.6", 73 | "yarn": "^1.22.0" 74 | }, 75 | "browserslist": "last 3 Chrome versions" 76 | } 77 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // This is the background script 2 | // accessible in other components via chrome.runtime.getBackgroundPage() 3 | 4 | import { broadcastMessage, listenForMessages } from '@/lib/message-passing'; 5 | import { setSettings, getSetting } from '@/lib/storage-helpers'; 6 | 7 | import './manifest'; // this is only to get the linter to run on it 8 | 9 | const tabs = {}; 10 | window.tabs = tabs; 11 | 12 | // Install handler 13 | chrome.runtime.onInstalled.addListener(async () => { 14 | // await setSettings({ 15 | // setting1: 'Initial value setting 1', 16 | // setting2: 'Initial value setting 2', 17 | // }); 18 | }); 19 | 20 | const WEB3_ACTIONS = ['enable', 'send']; 21 | 22 | listenForMessages((payload, sender, reply) => { // eslint-disable-line consistent-return 23 | console.log('👂 background heard runtime message'); 24 | console.log('sender: ', sender.tab ? `TAB #${sender.tab.id}` : sender); 25 | console.log('payload: ', payload); 26 | 27 | const { action } = payload; 28 | 29 | let tabId; 30 | if (sender.tab) tabId = sender.tab.id; 31 | else if (payload._inspectedTabId) tabId = payload._inspectedTabId; 32 | 33 | if (tabId) tabs[tabId] = tabs[tabId] || {}; 34 | 35 | // Handle actions that do not tell us that 36 | if (action === 'check_devtools_enabled') { 37 | console.log('replying with: ', tabs[tabId].enabled); 38 | return reply(tabs[tabId].enabled); 39 | } else if (action === 'page_reload') { 40 | tabs[tabId].history = []; 41 | } else if (action === 'fetch_events_history') { 42 | console.log('replying with:', tabs[tabId].history); 43 | return reply(tabs[tabId].history); 44 | } else if (WEB3_ACTIONS.includes(action)) { 45 | tabs[tabId].enabled = true; 46 | tabs[tabId].history = tabs[tabId].history || []; 47 | tabs[tabId].history.push(payload); 48 | 49 | // enable the icon if not already 50 | chrome.browserAction.setIcon({ 51 | tabId, 52 | path: { 53 | 128: '/icons/128-enabled.png', 54 | }, 55 | }); 56 | } 57 | }); 58 | 59 | // last minute cleanup 60 | chrome.runtime.onSuspend.addListener(() => { 61 | // do not rely on this for persisitng data 62 | console.log('Unloading extension'); 63 | }); 64 | 65 | 66 | // omnibox handler - see `omnibox` definition in manifest 67 | chrome.omnibox.onInputEntered.addListener((text) => { 68 | const newURL = `https://www.google.com/search?q=${encodeURIComponent(text)}`; 69 | chrome.tabs.create({ url: newURL }); 70 | }); 71 | 72 | // trigger this from the background console to test sending a message 73 | window.sendTestMessage = () => { 74 | console.log(chrome.tabs); 75 | 76 | // chrome.tabs.query({ active: true }, (tabs) => { 77 | // chrome.tabs.sendMessage(tabs[0].id, { 78 | // source: 'myextension', 79 | // message: 'background says hi!', 80 | // }, (response) => { 81 | // console.log(response); 82 | // }); 83 | // }); 84 | }; 85 | -------------------------------------------------------------------------------- /src/devtools-panel/tabs/logs/single-log.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 67 | 68 | 69 | 110 | -------------------------------------------------------------------------------- /src/lib/message-passing.js: -------------------------------------------------------------------------------- 1 | import _isString from 'lodash/isString'; 2 | import _get from 'lodash/get'; 3 | 4 | 5 | // used to broadcast messages from any extension components 6 | export async function broadcastMessage(payload) { 7 | return new Promise((resolve, reject) => { 8 | // if broadcasting from a webpage (not a content script, but one actually injected into the page) 9 | // then we cannot use chrome.runtime.sendMessage, and must instead use window.postMessage 10 | // to pass our message via the content script 11 | 12 | const fullPayload = { 13 | _msgSource: process.env.EXTENSION_MESSAGE_ID, 14 | ..._isString(payload) ? { message: payload } : payload, 15 | }; 16 | 17 | if (chrome.devtools) { 18 | fullPayload._inspectedTabId = chrome.devtools.inspectedWindow.tabId; 19 | } 20 | 21 | // detect if we are in a webpage 22 | if (!chrome.runtime.id) { 23 | // console.log('> sending message from webpage', fullPayload, window.origin); 24 | // pass the message via the window to our injector script which will relay it 25 | window.postMessage(JSON.stringify(fullPayload), window.origin); 26 | } else { 27 | // console.log('> sending message from chrome.runtime.sendMessage', fullPayload); 28 | chrome.runtime.sendMessage(process.env.EXTENSION_ID, fullPayload, (response) => { 29 | console.log('< response from extension', response); 30 | resolve(response); 31 | }); 32 | } 33 | }); 34 | } 35 | 36 | 37 | export function listenForMessages(messageHandler) { 38 | chrome.runtime.onMessage.addListener((payload, sender, reply) => { 39 | // ignore messages that are not from our extension 40 | if (payload._msgSource !== process.env.EXTENSION_MESSAGE_ID) return; 41 | 42 | messageHandler(payload, sender, reply); 43 | }); 44 | } 45 | export function listenForMessagesFromTab(tabId, messageHandler) { 46 | chrome.runtime.onMessage.addListener((payload, sender, reply) => { 47 | // ignore messages that are not from our extension 48 | if (payload._msgSource !== process.env.EXTENSION_MESSAGE_ID) return; 49 | if (_get(sender, 'tab.id') !== tabId) return; 50 | 51 | messageHandler(payload, sender, reply); 52 | }); 53 | } 54 | 55 | 56 | // Scripts injected into the actual page (ex: injected.js) cannot communicate direcctly with our extension 57 | // so we must use window.postMessage to send a message, and our content script (injector.js) must relay 58 | // that message back to our extension using chrome.runtime.sendMessage 59 | export function initializeWebpageMessageRelayer() { 60 | window.addEventListener('message', (e) => { 61 | if (!e.data) return; 62 | let messageData; 63 | try { 64 | messageData = JSON.parse(e.data); 65 | } catch (err) { 66 | return; 67 | } 68 | 69 | if (messageData._msgSource !== process.env.EXTENSION_MESSAGE_ID) return; 70 | chrome.runtime.sendMessage(process.env.EXTENSION_ID, messageData); 71 | }); 72 | } 73 | 74 | export function listenForOpenResources(resourceHandler) { 75 | chrome.devtools.panels.setOpenResourceHandler((resource) => { 76 | resourceHandler(resource); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /src/manifest.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is the source for our "manifest.json" file 3 | But we transform it from js so we can include comments and eventually (maybe?) build it dynamically 4 | 5 | WARNING - auto reload doesn't work with this, so you must restart webpack after you make changes 6 | */ 7 | 8 | module.exports = { 9 | manifest_version: 2, // required 10 | 11 | ...process.env.NODE_ENV === 'production' ? { 12 | name: 'ETH Dev Tools', // Title in chrome store and extensions settings UI 13 | } : { 14 | name: 'ETH Dev Tools (dev)', 15 | key: process.env.EXTENSION_KEY, 16 | }, 17 | version: '0.0.1', 18 | description: 'Developer tools panel for web3 developers', 19 | homepage_url: 'https://ethdevtools.xyz', 20 | icons: { 21 | // 16: 'icons/16.png', 22 | // 48: 'icons/48.png', 23 | 128: 'icons/128-enabled.png', 24 | }, 25 | permissions: [ // see https://developer.chrome.com/extensions/declare_permissions 26 | '', 27 | '*://*/*', 28 | 'activeTab', // required to inject scripts in current tab 29 | 'tabs', 30 | 'background', 31 | 'contextMenus', // for context menu - make sure to include a size 16 icon for this 32 | 'unlimitedStorage', 33 | 'storage', 34 | 'notifications', 35 | 'identity', 36 | 'identity.email', 37 | ], 38 | incognito: 'spanning', // spanning|split -- see https://developer.chrome.com/extensions/manifest/incognito 39 | 40 | // background script - used to manage events and initialize other parts of the extensions 41 | // see https://developer.chrome.com/extensions/background_pages 42 | background: { 43 | scripts: ['js/background.js'], 44 | persistent: false, 45 | }, 46 | 47 | // A "browser action" is the icon that appears to the right of the toolbar and is always available 48 | // see https://developer.chrome.com/extensions/browserAction 49 | // you can also switch to "page_action" if it is only applicable to the current page 50 | // https://developer.chrome.com/extensions/pageAction 51 | browser_action: { 52 | default_title: 'Action icon hover title', // tooltip text when hovering over the icon 53 | default_popup: 'popup.html', // content of "popup" that appears when you click the icon 54 | default_icon: { 55 | 128: 'icons/128-disabled.png', // we start the icon disabled and enable it in background 56 | }, 57 | }, 58 | 59 | // extension options page 60 | options_ui: { 61 | page: 'options.html', 62 | open_in_tab: true, // set to false to show in a popup instead 63 | }, 64 | 65 | // omnibox - this lets you hook into the omnnibox (url bar) by typing this string first 66 | // see background script for handler 67 | omnibox: { 68 | keyword: 'eth', // this should be short to trigger your extension to take over 69 | }, 70 | 71 | // inject scripts into pages being browsed 72 | content_scripts: [ 73 | { 74 | js: ['js/injector.js'], 75 | run_at: 'document_start', // "document_idle" preferred, but would not guarantee running first 76 | matches: [''], 77 | all_frames: true, // otherwise just injected into main/topmost frame 78 | }, 79 | ], 80 | 81 | // Extend chrome devtools 82 | // this page is not shown, it initializes other devtools components (panels, sidebars, etc) 83 | devtools_page: 'devtools.html', // it must be an html page for some reason 84 | 85 | content_security_policy: "script-src 'self' 'unsafe-eval'; object-src 'self'", 86 | web_accessible_resources: [ 87 | 'devtoolspanel.html', 88 | 'js/injected.js', 89 | ], 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /src/style/typography.less: -------------------------------------------------------------------------------- 1 | 2 | @h1-font-size: 44; 3 | @h2-font-size: 36; 4 | @h3-font-size: 26; 5 | @h4-font-size: 22; 6 | @h5-font-size: 12; 7 | @h6-font-size: 10; 8 | 9 | 10 | body, html { 11 | color: @bluegray-text; 12 | font-family: @regular-font; 13 | font-weight: normal; 14 | font-style: normal; 15 | line-height: @default-line-height*100%; 16 | 17 | -webkit-font-smoothing: antialiased; 18 | -moz-osx-font-smoothing: grayscale; 19 | } 20 | 21 | 22 | /* Default header styles */ 23 | h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 { 24 | // font-weight: normal; 25 | // .light-weight(); 26 | margin: .5em 0; 27 | line-height: 1.5em; 28 | color: @black; 29 | font-family: @fancy-font; 30 | position: relative; 31 | 32 | small { 33 | font-size: .85em; 34 | line-height: 0; 35 | } 36 | 37 | &.not-fancy { 38 | font-family: @regular-font; 39 | } 40 | } 41 | h1, .h1 { .font-rem(@h1-font-size); } 42 | h2, .h2 { .font-rem(@h2-font-size); } 43 | h3, .h3 { .font-rem(@h3-font-size); } 44 | h4, .h4 { .font-rem(@h4-font-size); margin: 0; } 45 | h5, .h5 { .font-rem(@h5-font-size); margin: 0; } 46 | h6, .h6 { .font-rem(@h6-font-size); margin: 0; } 47 | 48 | // .subheader { @include subheader; } 49 | 50 | /* Default Paragraph Styles */ 51 | p { 52 | margin-bottom: 1em; 53 | line-height: @default-line-height-em; 54 | text-rendering: optimizeLegibility; 55 | &.small { 56 | font-size: .85em; 57 | } 58 | &.narrow { 59 | max-width: 32em; 60 | margin: 1em auto; 61 | 62 | @media @mq-small-only { 63 | padding: 0 1.5em; 64 | } 65 | } 66 | } 67 | 68 | .small{ 69 | font-size: .85em; 70 | line-height: 1.4em; 71 | } 72 | .tiny{ 73 | font-size: .7em; 74 | line-height: 1.4em; 75 | } 76 | 77 | 78 | /* Default Link Styles */ 79 | a { 80 | text-decoration: none; 81 | transition: color .2s; 82 | color: #145bce; 83 | } 84 | p a { 85 | text-decoration: underline; 86 | color: currentColor; 87 | 88 | &.no-decoration { 89 | text-decoration: none; 90 | } 91 | &:hover, &:focus { 92 | color: #000; 93 | } 94 | } 95 | 96 | 97 | /* Other Helpful Defaults */ 98 | em, i { 99 | .italic(); 100 | line-height: inherit; 101 | } 102 | 103 | strong, b, .strong { 104 | .bold(); 105 | line-height: inherit; 106 | } 107 | 108 | small, .small { 109 | .font-rem(14); 110 | line-height: inherit; 111 | } 112 | 113 | .text-left { 114 | text-align: left; 115 | } 116 | 117 | 118 | /* More header styles */ 119 | h2.underlined, 120 | h3.underlined { 121 | position: relative; 122 | 123 | &:after { 124 | content: ''; 125 | position: absolute; 126 | bottom: -8px; 127 | left: 0; 128 | height: 5px; 129 | width: 34px; 130 | background-color: currentColor; 131 | } 132 | } 133 | 134 | h5 { 135 | .small-title(); 136 | color: #555; 137 | } 138 | 139 | 140 | // Helper classes 141 | .font-rem(@size) { 142 | font-size: unit(@size, px); 143 | font-size: unit(@size/@default-font-size, rem); 144 | } 145 | .light-weight { 146 | // We dont have a light weight for now 147 | font-weight: 400; 148 | font-style: normal; 149 | } 150 | .regular-weight { 151 | font-weight: 400; 152 | font-style: normal; 153 | } 154 | .bold { 155 | // font-family: @bold-font; 156 | font-weight: 600; 157 | font-style: normal; 158 | } 159 | .italic { 160 | font-style: italic; 161 | } 162 | 163 | .fancy-font() { 164 | font-family: @fancy-font; 165 | } 166 | 167 | .small-title { 168 | .bold(); 169 | text-transform: uppercase; 170 | font-size: 12px; 171 | } 172 | 173 | .uppercase { 174 | text-transform: uppercase; 175 | } 176 | 177 | .truncate-text { 178 | white-space: nowrap; 179 | text-overflow: ellipsis; 180 | overflow: hidden; 181 | } 182 | .nowrap { 183 | white-space: nowrap; 184 | } 185 | 186 | .align-left { text-align: left; } 187 | .align-right { text-align: right; } 188 | .align-center { text-align: center; } 189 | -------------------------------------------------------------------------------- /src/injected.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, prefer-destructuring */ 2 | 3 | /* 4 | This script is injected into the page by injector.js 5 | so that it is able to access the same global `window` object that the actual webpage does 6 | 7 | We set up a new web3 provider that just proxies to the real one but sends logging info 8 | to our extension 9 | */ 10 | 11 | 12 | // IMPORTANT NOTE - USE REQUIRE INSTEAD OF IMPORT! 13 | // have not been able to figure out why, but importing any file breaks the proxying setup 14 | // while using require does not... ¯\_(ツ)_/¯ 15 | const { broadcastMessage } = require('@/lib/message-passing'); 16 | 17 | function createProxyProvider() { 18 | console.log('INITIALIZING PROXY PROVIDER'); 19 | 20 | let currentProvider; 21 | if (window.ethereum) { 22 | console.log('found window.ethereum', window.ethereum); 23 | currentProvider = window.ethereum; 24 | } else if (window.web3) { 25 | console.log('found window.web3', window.web3); 26 | console.log('found window.web3.currentProvider', window.web3.currentProvider); 27 | currentProvider = window.web3.currentProvider; 28 | } else { 29 | console.log('web3 not found :('); 30 | return; 31 | } 32 | 33 | window.devtoolsProxyProvider = new Proxy(currentProvider, { 34 | get(target, key, context) { 35 | console.log('DEVTOOLS PROXY GET', key); 36 | if (key === 'enable') { 37 | const _originalEnable = target[key]; 38 | return function newEnable(...args) { 39 | console.log('!!! web3 enable logged', args); 40 | broadcastMessage({ action: 'enable', args }); 41 | return _originalEnable(...args); 42 | }; 43 | } else if (key === 'send') { 44 | console.log(target, context); 45 | const _originalSend = target[key]; 46 | 47 | return function w3dtSend(...args) { 48 | // const requestId = Math.floor(Math.random() * 1000000); 49 | console.log('!!! web3 send logged', args); 50 | broadcastMessage({ action: 'send', method: args[0], args }); 51 | 52 | const returnOfOriginalSend = _originalSend(...args); 53 | console.log(returnOfOriginalSend); 54 | return returnOfOriginalSend; 55 | }; 56 | } else if (key === 'sendAsync') { 57 | // sendAsync calls rely on a callback 58 | // so we must swap out the original callback to catch the result 59 | 60 | const _originalSend = target[key]; 61 | 62 | return function w3dtSendAsync(...args) { 63 | console.log('!!! web3 sendAsync logged', args); 64 | const requestId = args[0].id; 65 | broadcastMessage({ 66 | action: 'send', 67 | requestId, 68 | time: +new Date(), 69 | method: args[0].method, 70 | params: args[0].params, 71 | }); 72 | 73 | const originalCallback = args.pop(); 74 | const w3dtSendAsyncCallback = function (...callbackArgs) { 75 | console.log('!!! web3 sendAsync callback!', callbackArgs); 76 | broadcastMessage({ 77 | action: 'send_result', 78 | requestId: callbackArgs[1].id, 79 | result: callbackArgs[1].result, 80 | }); 81 | originalCallback(...callbackArgs); 82 | }; 83 | args.push(w3dtSendAsyncCallback); 84 | 85 | return _originalSend(...args); 86 | }; 87 | } 88 | return Reflect.get(...arguments); 89 | }, 90 | set(target, key, value) { 91 | if (key === 'currentProvider') { 92 | console.log('PROXY - setting current provider'); 93 | } 94 | // target[key] = value; 95 | return Reflect.set(target, key, value); 96 | }, 97 | }); 98 | console.log('created proxy provider', window.devtoolsProxyProvider); 99 | } 100 | 101 | function attemptPatchWeb3() { 102 | console.log('PATCHING WEB3'); 103 | // replace provider with our proxy 104 | console.log('window.web3', window.web3); 105 | console.log('replacing web3.currentProvider', window.web3.currentProvider); 106 | window.web3.currentProvider = window.devtoolsProxyProvider; 107 | console.log('replacing web3.ethereum', window.web3.ethereum); 108 | window.ethereum = window.devtoolsProxyProvider; 109 | console.log('replaced web3 with our proxy', window.web3.currentProvider, window.ethereum); 110 | } 111 | 112 | createProxyProvider(); 113 | attemptPatchWeb3(); 114 | -------------------------------------------------------------------------------- /src/devtools-panel/tabs/logs/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 90 | 91 | 169 | -------------------------------------------------------------------------------- /src/devtools-panel/store.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, prefer-destructuring */ 2 | import _ from 'lodash'; 3 | import Vue from 'vue'; 4 | import Vuex from 'vuex'; 5 | 6 | // import { AbiCoder } from 'web3-eth-abi'; 7 | import AbiDecoder from 'abi-decoder'; 8 | 9 | 10 | import { broadcastMessage, listenForMessagesFromTab, listenForOpenResources } from '@/lib/message-passing'; 11 | 12 | 13 | Vue.use(Vuex); 14 | 15 | // window.AbiDecoder = AbiDecoder; 16 | // const abiCoder = new AbiCoder(); 17 | 18 | let logCounter = 0; 19 | 20 | const store = new Vuex.Store({ 21 | state: { 22 | inspectedTabId: chrome.devtools.inspectedWindow.tabId, 23 | logs: {}, // will be keyed by id (counter) 24 | contracts: {}, // keyed by address 25 | accounts: {}, 26 | }, 27 | getters: { 28 | logs: (state) => _.orderBy(_.values(state.logs), 'time'), 29 | contracts: (state) => _.values(state.contracts), 30 | }, 31 | mutations: { 32 | CLEAR_LOGS: (state) => { 33 | state.logs = {}; 34 | }, 35 | CLEAR_CONTRACTS: (state) => { 36 | state.contracts = {}; 37 | }, 38 | ADD_MESSAGE_LOG: (state, payload) => { 39 | const id = logCounter++; 40 | Vue.set(state.logs, `message|${id}`, { 41 | time: +new Date(), 42 | type: 'message', 43 | message: payload.message, 44 | payload, 45 | }); 46 | }, 47 | ADD_SEND_LOG: (state, payload) => { 48 | console.log('ADD_SEND_LOG', payload); 49 | 50 | // payload { requestId, time, method, params} 51 | const log = { 52 | ...payload, 53 | 54 | }; 55 | 56 | Vue.set(state.logs, payload.requestId, log); 57 | 58 | 59 | // payload.data.id 60 | // payload.data.method 61 | // payload.data.params 62 | // if (data.result !== undefined) { // can be null! 63 | // data.resultTime = +new Date(); 64 | // const req = state.logs[`req|${data.id}`]; 65 | // const annotatedResult = getAnnotatedResult(req, data.result); 66 | // if (typeof data.result !== 'undefined') Vue.set(state.logs[`req|${data.id}`], 'result', data.result); 67 | // Vue.set(state.logs[`req|${data.id}`], 'annotatedResult', annotatedResult); 68 | // Vue.set(state.logs[`req|${data.id}`], 'resultTime', +new Date()); 69 | // if (req.method === 'eth_accounts') { 70 | // state.accounts = data.result; 71 | // } 72 | // state.sends.push(data.result); 73 | // } else { 74 | // data.type = 'send'; 75 | // data.time = +new Date(); 76 | // annotateParams(data); 77 | // Vue.set(state.logs, `req|${data.id}`, data); 78 | 79 | // // const processLogMessage = processMethod[data.method] || processMethod.default; 80 | // // const logMessage = processLogMessage(data.params, data.method, state.contracts); 81 | // // logMessage.method = data.method; 82 | // // logMessage.id = data.id; 83 | // // logMessage.time = +new Date(); 84 | // // logMessage.args = data.params; 85 | // // state.sends.push(logMessage); 86 | // // Vue.set(state.logs, `send|${data.id}`, logMessage); 87 | // } 88 | }, 89 | UPDATE_LOG_RESULT: (state, payload) => { 90 | // const { args } = state.sends.find((s) => s.id === payload.id); 91 | // const method = args[0]; 92 | // const processLogResult = processResult[method] || processResult.default; 93 | // const logResult = processLogResult(args, payload.results, method, state.contracts); 94 | // logResult.id = payload.id; 95 | // logResult.time = +new Date(); 96 | // logResult.method = payload.method; 97 | Vue.set(state.logs[payload.requestId], 'result', payload.result); 98 | console.log(state.logs[payload.requestId]); 99 | // Vue.set(state.logs[`send|${payload.id}`], 'response', payload.response); 100 | 101 | // if (args[0] === 'eth_accounts') { 102 | // Vue.set(state.accounts, 'accounts', logResult.params); 103 | // } 104 | }, 105 | ADD_CONTRACT: (state, payload) => { 106 | console.log('ADD_CONTRACT', { payload }); 107 | const newContracts = state.contracts.concat(payload); 108 | state.contracts = newContracts; 109 | AbiDecoder.addABI(payload); 110 | }, 111 | }, 112 | actions: { 113 | async initializeAndGetHistory(ctx) { 114 | // ctx.commit('ADD_MESSAGE_LOG', { message: 'devtools startup - fetch history' }); 115 | const pastEvents = await broadcastMessage({ action: 'fetch_events_history' }); 116 | _.each(pastEvents, (event) => { 117 | ctx.dispatch('processEvent', event); 118 | }); 119 | }, 120 | processEvent(ctx, payload) { 121 | const { action } = payload; 122 | if (action === 'page_reload') { 123 | ctx.commit('CLEAR_LOGS', payload); 124 | } else if (action === 'contract') { 125 | ctx.commit('ADD_CONTRACT', payload); 126 | } else if (action === 'send') { 127 | const log = _.pick(payload, [ 128 | 'requestId', 'time', 'action', 'method', 129 | ]); 130 | if (payload.method === 'eth_call') { 131 | log.contractAddress = payload.params[0].to; 132 | log.params = payload.params[0].data; 133 | log.blockNumber = payload.params[1]; 134 | } else if (payload.method === 'eth_accounts') { 135 | 136 | } 137 | 138 | 139 | console.log(payload.params); 140 | // if (payload.method === 'eth_call') { 141 | // log.contractAddress = payload.params[0].to; 142 | // log.params = payload.params[0].data; 143 | // } 144 | store.commit('ADD_SEND_LOG', log); 145 | } else if (action === 'send_result') { 146 | store.commit('UPDATE_LOG_RESULT', payload); 147 | } else if (action === 'log') { 148 | store.commit('ADD_MESSAGE_LOG', payload); 149 | } 150 | }, 151 | processContract(ctx, abiJSON) { 152 | ctx.commit('ADD_CONTRACT', abiJSON); 153 | }, 154 | logMessage(ctx, payload) { 155 | console.log('log message action'); 156 | ctx.commit('ADD_MESSAGE_LOG', payload); 157 | }, 158 | }, 159 | }); 160 | 161 | store.dispatch('initializeAndGetHistory'); 162 | 163 | 164 | listenForMessagesFromTab(chrome.devtools.inspectedWindow.tabId, (payload, sender, reply) => { 165 | console.log('👂 devtools panel store heard runtime message'); 166 | console.log(payload); 167 | store.dispatch('processEvent', payload); 168 | }); 169 | 170 | listenForOpenResources((resource) => { 171 | resource.getContent((content) => { 172 | console.log('resource opened: ', content); 173 | let contentJSON; 174 | try { 175 | const contentJSON = JSON.parse(content); 176 | console.log(contentJSON); 177 | store.dispatch('processContract', contentJSON); 178 | } catch (err) { 179 | 180 | } 181 | }); 182 | }); 183 | 184 | export default store; 185 | --------------------------------------------------------------------------------