├── .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 |
2 | div
3 | h2 Dev tools tab 2
4 | div(v-for='(contract, index) in contracts' :key="`contract-${index}`")
5 |
6 |
28 |
30 |
--------------------------------------------------------------------------------
/src/popup/popup-page.vue:
--------------------------------------------------------------------------------
1 |
2 | #action-popup-pane
3 | //- TODO: display message if web3 has been detected or not
4 | div ETHDevTools
5 | button(@click='buttonClickHandler') Send test message
6 |
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 |
2 | #options-page
3 | h2 My Extension Options Page
4 |
5 | label
6 | span.input-label Setting #1
7 | input(type='text' v-model='setting1')
8 | label
9 | span.input-label Setting #2
10 | input(type='text' v-model='setting2')
11 | button(@click='saveButtonHandler') Save settings
12 |
13 |
14 |
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 |
2 | .devtools-panel
3 | nav.devtools-nav.native-bar
4 | a(href="https://github.com/theoephraim/ethdevtools/" target="_blank") ⚒ Eth Dev Tools ⚒
5 | router-link(:to='{name: "logs"}') Logs
6 | router-link(:to='{name: "contracts"}') Contracts
7 | router-view
8 | //- template(v-else)
9 | //- button(id="connect" @click.stop="connect") connect to web3
10 |
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 |
2 | .log.flex
3 | template(v-if='log.action === "send"')
4 | .col.name
5 | | {{ log.method }}
6 | span.repeat-count(v-if='log.count > 1') {{ log.count }}
7 | div(v-if='log.callName') {{ log.callName }}
8 | .col.time
9 | | {{ log.time | logtime }}
10 | .response-time(v-if='resultDelay') result +{{resultDelay}}ms
11 | .col.params.m1
12 | json(v-if='paramsData' :deep="deep" :data='paramsData')
13 | .col.returns.m1
14 | json(v-if='resultData' :deep="deep" :data='resultData')
15 | template(v-else-if='log.type === "message"')
16 | .col.name MESSAGE
17 | .col.time {{ log.time | logtime }}
18 | .col.m1.details {{ log.message }}
19 | template(v-else-if='log.type === "contract"')
20 | .col.name CONTRACT LOADED
21 | .col.time {{ log.time | logtime }}
22 | .col.m1.details Address: {{ log.address }}
23 |
24 |
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 |
2 | .logs-viewer
3 | .settings.native-bar
4 | span.no-move
5 | a.icon.clear-button(href='#' @click.prevent='clearLogs' title='Clear Logs')
6 | svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-ban fa-w-16 fa-3x")
7 | path(fill="currentColor" d="M256 8C119.034 8 8 119.033 8 256s111.034 248 248 248 248-111.034 248-248S392.967 8 256 8zm130.108 117.892c65.448 65.448 70 165.481 20.677 235.637L150.47 105.216c70.204-49.356 170.226-44.735 235.638 20.676zM125.892 386.108c-65.448-65.448-70-165.481-20.677-235.637L361.53 406.784c-70.203 49.356-170.226 44.736-235.638-20.676z")
8 | span
9 | label
10 | input(type='checkbox' v-model='groupSimilar')
11 | span Group similar
12 | span
13 | b.mr1 Hide:
14 | label
15 | input(type='checkbox' v-model='hideEthAccounts')
16 | span eth_accounts
17 | label
18 | input(type='checkbox' v-model='hideNetVersion')
19 | span net_version
20 | label
21 | input(type='checkbox' v-model='hideEthGetBalance')
22 | span eth_getBalance
23 | .header.flex.native-bar
24 | .col.name Name
25 | .col.time Time
26 | .col.grow-1.mx2 Params
27 | .col.grow-1.mx2 Result
28 | .logs
29 | single-log(v-for='log in condensedLogs' :log='log' :key="`${log.requestId}+${log.time}`")
30 | //- logs
31 |
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 |
--------------------------------------------------------------------------------