├── .editorconfig ├── CHANGELOG.md ├── README.md ├── generator ├── index.js └── templates │ ├── index.js │ └── module │ ├── actions.js │ ├── ethersConnect.js │ ├── getters.js │ ├── index.js │ └── mutations.js ├── index.js ├── package.json └── prompts.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## 0.1 (2018-09-09) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Ethereum ethers.js vuex store module generator plugin for vue-cli 3 9 | #### Build dApps! 10 | 11 | [vue-cli 3](https://github.com/vuejs/vue-cli) plugin to generate a vuex store module that will connect to the Ethereum blockchain using the [ethers.js](https://github.com/ethers-io/ethers.js/) web3 provider. (ethers.js can do everything [web3.js](https://github.com/ethereum/web3.js/) can do and more, like ENS name resolution. It is also well documented.) 12 | 13 | This plugin monitors and updates connection and network status with your Ethereum node on any network and allows you to connect to contracts and submit transactions. 14 | 15 | (Requires a browser that supports Ethereum or the use of a browser plugin like [MetaMask](https://metamask.io/).) Can serve as the foundation of a vue-based Ethereum dApp! 16 | 17 | ### State 18 | These state variables are kept updated: 19 | * connected (true or false) 20 | * address (of the user) 21 | * ens (ENS name if any for selected address) 22 | * user (either address or if on a net that supports ENS, the ENS name) 23 | * error (error if any) 24 | * network (human readable name of the network you are on) 25 | 26 | ### Example code 27 | This plugin comes with the following example code enabled so you can see how this module works. Replace the example code with your own application logic. 28 | * In the store module, search for the `alert` and `confirm` statements. These will alert the state of the ethereum connection and account changes in the browser. 29 | * By default ethersConnect.js is set to log each block of the blockchain in the console. Review the code comments to see how to easily extend or disable this. 30 | 31 | ### Usage 32 | 33 | * See [Vuex Getting Started](https://vuex.vuejs.org/guide/) for general information about Vuex. 34 | * This plugin expects you to have a file `main.js` in your `src` folder according to vue-cli standard. 35 | * It also expects you to have already added Vuex to your project before adding this plugin. 36 | * And that 37 | * your store folder is called ```store ``` with an `index.js` in it, 38 | * you have a ```modules``` section in your store `index.js`, 39 | * and you have an ```export``` in your store `index.js` file. 40 | 41 | For example, the standard store created by `vue add vuex`, with a modules section added, will work: 42 | 43 | ``` 44 | import Vue from 'vue' 45 | import Vuex from 'vuex' 46 | 47 | Vue.use(Vuex) 48 | 49 | export default new Vuex.Store({ 50 | state: { 51 | }, 52 | mutations: { 53 | }, 54 | actions: { 55 | }, 56 | modules: { 57 | } 58 | }) 59 | ``` 60 | 61 | These are needed to correctly augment the root store with the ethers module code. 62 | 63 | 64 | ### Install via vue-cli 65 | 66 | ```sh 67 | $ vue add ethers 68 | ``` 69 | 70 | 71 | ### Interacting with contracts 72 | Read about how to do this in the [ethers.js contract documentation](https://docs.ethers.io/v5/api/contract/contract/). 73 | 74 | ### Contributions 75 | Pull requests welcome! 76 | 77 | 78 | 79 | #### Acknowledgements 80 | Extended and modified from [vue-cli-plugin-vuex-module-generator](https://github.com/paulgv/vue-cli-plugin-vuex-module-generator). Thanks! 81 | -------------------------------------------------------------------------------- /generator/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = (api, options, rootOptions) => { 5 | // const {} = options; 6 | const moduleName = 'ethers' 7 | const storeRootDir = 'store' 8 | const templatesRoot = './templates'; 9 | const moduleDirPath = path.join('./src', storeRootDir, moduleName); 10 | const storeRootPath = path.join('./src', storeRootDir, 'index.js'); 11 | const mainJsPath = path.join('./src/main.js'); 12 | 13 | // Abort if module already exists 14 | if (fs.existsSync(moduleDirPath)) { 15 | console.warn(`Module ${moduleName} exists`) 16 | return; 17 | } 18 | 19 | const files = {}; 20 | 21 | // Store root 22 | if (!fs.existsSync(storeRootPath)) { 23 | files[storeRootPath] = `${templatesRoot}/index.js`; 24 | } 25 | 26 | // Modules templates 27 | ['index', 'actions', 'mutations', 'getters', 'ethersConnect'].forEach(template => { 28 | const fileName = `${template}.js`; 29 | const filePath = path.join(moduleDirPath, fileName); 30 | files[filePath] = `${templatesRoot}/module/${fileName}`; 31 | }); 32 | 33 | api.extendPackage({ 34 | dependencies: { 35 | vuex: '^3.5.1', 36 | ethers: '^5.0.8' 37 | } 38 | }); 39 | 40 | api.render(files); 41 | 42 | api.postProcessFiles(files => { 43 | // Edit store's root module 44 | const storeRoot = files[storeRootPath]; 45 | 46 | if (storeRoot) { 47 | const lines = storeRoot.split(/\r?\n/g).reverse(); 48 | 49 | // Add import line 50 | const lastImportIndex = lines.findIndex(line => line.match(/^import/)); 51 | if (lastImportIndex !== -1) { 52 | lines[lastImportIndex] += `\nimport ${moduleName} from './${moduleName}'`; 53 | } 54 | 55 | // Add module line 56 | lines.reverse() 57 | const modulesStartIndex = lines.findIndex(line => line.match(/modules: *{/)); 58 | if (modulesStartIndex !== -1) { 59 | const spaces = lines[modulesStartIndex].indexOf('modules'); 60 | const modulesEndIndex = lines.findIndex((line, index) => index >= modulesStartIndex && line.match(/}/)); 61 | if (modulesEndIndex !== -1) { 62 | if (modulesEndIndex === modulesStartIndex) { 63 | const closingBraceIndex = lines[modulesStartIndex].indexOf('}'); 64 | const start = lines[modulesStartIndex].substr(0, closingBraceIndex); 65 | const end = lines[modulesStartIndex].substr(closingBraceIndex); 66 | lines[modulesEndIndex] = `${start}\n${Array(spaces + 3).join(' ')}${moduleName}\n${Array(spaces + 1).join(' ')}${end}`; 67 | } else { 68 | lines[modulesEndIndex] = `${Array(spaces + 3).join(' ')}${moduleName}\n${lines[modulesEndIndex]}`; 69 | if (modulesEndIndex - modulesStartIndex > 1) { 70 | lines[modulesEndIndex - 1] += ','; 71 | } 72 | } 73 | } 74 | } 75 | files[storeRootPath] = lines.join('\n'); 76 | } 77 | }) 78 | 79 | 80 | api.onCreateComplete(() => { 81 | // augment main.js to init ethers store 82 | let contentMain = fs.readFileSync(mainJsPath, { encoding: 'utf-8' }) 83 | const linesMain = contentMain.split(/\r?\n/g) 84 | 85 | linesMain.push('\n// Initialize ethers store') 86 | linesMain.push(`store.dispatch('ethers/init')\n`) 87 | contentMain = linesMain.join('\n') 88 | fs.writeFileSync(mainJsPath, contentMain, { encoding: 'utf-8' }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /generator/templates/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | const store = new Vuex.Store({ 7 | modules: {} 8 | }) 9 | 10 | export default store 11 | -------------------------------------------------------------------------------- /generator/templates/module/actions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { 3 | MSGS, 4 | EVENT_CHANNEL, 5 | connect, 6 | event, 7 | ready, 8 | getProvider, 9 | getWallet, 10 | getWalletAddress, 11 | getNetName, 12 | hasEns 13 | } from './ethersConnect' 14 | 15 | 16 | 17 | export default { 18 | async connect(ctx) { 19 | try { 20 | const oldAddress = ctx.state.address 21 | const oldNetwork = ctx.state.network 22 | 23 | const provider = getProvider() 24 | if (!provider) throw new Error(MSGS.NOT_CONNECTED) 25 | 26 | const wallet = getWallet() 27 | if (!wallet) throw new Error(MSGS.NO_WALLET) 28 | const address = await getWalletAddress() 29 | const network = await getNetName() 30 | 31 | if (network !== oldNetwork || address !== oldAddress) { 32 | ctx.commit('connected', true) 33 | ctx.commit('error', null) 34 | ctx.commit('address', address) 35 | ctx.commit('user', address) 36 | ctx.commit('network', network) 37 | 38 | const msg = oldAddress && oldAddress !== address 39 | ? `Your Ethereum address/user has changed. 40 | Address: ${address} 41 | Network: ${network} 42 | Gas Price: ${await provider.getGasPrice()} 43 | Current Block #: ${await provider.getBlockNumber()} 44 | Your ether balance: ${await provider.getBalance(address)}` 45 | : `You are connected to the Ethereum Network. 46 | Address: ${address} 47 | Network: ${network} 48 | Gas Price: ${await provider.getGasPrice()} 49 | Current Block #: ${await provider.getBlockNumber()} 50 | Your ether balance: ${await provider.getBalance(address)} 51 | If you change your address or network, this app will update automatically.` 52 | alert(msg) 53 | 54 | // Other vuex stores can wait for this 55 | event.$emit(EVENT_CHANNEL, MSGS.ETHERS_VUEX_READY) 56 | 57 | // now check for .eth address too 58 | if (await hasEns()) { 59 | console.log('Net supports ENS. Checking...') 60 | ctx.commit('ens', await provider.lookupAddress(address)) 61 | if (ctx.state.ens) { 62 | ctx.commit('user', ens) 63 | } 64 | } 65 | 66 | provider.on('block', (blockNumber) => { 67 | console.log('Block mined:', blockNumber) 68 | }) 69 | } 70 | } catch (err) { 71 | ctx.dispatch('disconnect', err) 72 | } 73 | }, 74 | async disconnect(ctx, err) { 75 | const oldAddress = ctx.state.address 76 | ctx.commit('connected', false) 77 | ctx.commit('error', err) 78 | ctx.commit('address', '') 79 | ctx.commit('user', '') 80 | ctx.commit('network', '') 81 | ctx.commit('ens', null) 82 | 83 | const msg = err ? `There was an error: ${err.message}` : (oldAddress 84 | ? 'You have been disconnected from your Ethereum connection. Please check MetaMask, etc.' 85 | : 'You are not connected to an Ethereum node and wallet. Please check MetaMask, etc.') 86 | alert(msg) 87 | }, 88 | async logout(ctx) { 89 | ctx.commit('address', '') 90 | ctx.commit('user', '') 91 | alert('You have been logged out from your Ethereum connection') 92 | }, 93 | async notConnected(ctx) { 94 | ctx.commit('address', '') 95 | ctx.commit('user', '') 96 | alert('You are not connected to the Ethereum network. Please check MetaMask,etc.') 97 | }, 98 | async init(ctx) { 99 | 100 | event.$on(EVENT_CHANNEL, async function (msg) { 101 | console.log('Ethers event received', msg) 102 | switch (msg) { 103 | case MSGS.NOT_READY: 104 | await ctx.dispatch('disconnect') 105 | break 106 | case MSGS.NO_WALLET: 107 | await ctx.dispatch('logout') 108 | break 109 | case MSGS.ACCOUNT_CHANGED: 110 | await ctx.dispatch('connect') 111 | break 112 | case MSGS.NOT_CONNECTED: 113 | await ctx.dispatch('notConnected') 114 | break 115 | } 116 | }) 117 | 118 | if (ready()) { 119 | await ctx.dispatch('connect') 120 | event.$emit(EVENT_CHANNEL, MSGS.ETHERS_VUEX_INITIALIZED) 121 | } else { 122 | console.log('Log in to your Ethereum wallet to see what it can do!') 123 | // Usually should trigger connect on a user interaction as a best practice! 124 | // Replace this with a user button?? 125 | if (confirm('Welcome! You can replace "alert" and "confirm" statements in this vuex module with your own code to do more useful things. If you would like to connect to your Ethereum provider (e.g. MetaMask) for the first time click "Yes" now.')) { 126 | await connect() 127 | } 128 | } 129 | ctx.commit('initialized', true) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /generator/templates/module/ethersConnect.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Vue from 'vue' 3 | import { 4 | providers, 5 | Contract as ContractModule, 6 | utils as utilsModule 7 | } from 'ethers' 8 | 9 | export const PROVIDER_CHECK_MS = 500 10 | // networks where ens exists 11 | // Mainet, Ropsten, Ropsten 12 | export const ENS_NETS = ['0x1', '0x3', '0x4'] 13 | 14 | // messages 15 | export const MSGS = { 16 | NOT_CONNECTED: 'Not connected to Ethereum network', 17 | NOT_READY: 'Ethereum network not ready', 18 | NO_WALLET: 'No Ethereum wallet detected', 19 | ACCOUNT_CHANGED: 'Ethereum account changed', 20 | ETHERS_VUEX_INITIALIZED: 'Ethers vuex module initialized', 21 | ETHERS_VUEX_READY: 'Ethers vuex module ready' 22 | } 23 | export const EVENT_CHANNEL = 'ethers' 24 | // use vue as a simple event channel 25 | export const event = new Vue() 26 | // expose ethers modules 27 | export const utils = utilsModule 28 | export const Contract = ContractModule 29 | 30 | // ethereum transactions to log 31 | // More information: https://docs.ethers.io/ethers.js/html/api-providers.html#events 32 | export const LOG_TRANSACTIONS = [ 33 | 'block' 34 | // can also be an address or transaction hash 35 | // [] // list of topics, empty for all topics 36 | ] 37 | 38 | // for ethers 39 | let ethereum 40 | let provider 41 | let chainId 42 | let userWallet 43 | let currentAccount 44 | let providerInterval 45 | let initialized 46 | 47 | function getEthereum() { 48 | return window.ethereum 49 | } 50 | 51 | function ethereumOk() { 52 | const em = getEthereum() 53 | return em && em.isConnected() 54 | } 55 | 56 | // get the name of this network 57 | export async function getNetName() { 58 | switch (chainId) { 59 | case '0x1': 60 | return 'Mainnet'; 61 | case '0x2': 62 | return 'Morden (deprecated)' 63 | case '0x3': 64 | return 'Ropsten Test Network' 65 | case '0x4': 66 | return 'Rinkeby Test Network' 67 | case '0x5': 68 | return 'Goerli Test Network' 69 | case '0x2a': 70 | return 'Kovan Test Network'; 71 | case undefined: 72 | case null: 73 | return 'No Chain!' 74 | // if you give your ganache an id your can detect it here if you want 75 | default: 76 | return 'Unknown' 77 | } 78 | } 79 | 80 | // if this net has ens 81 | export async function hasEns() { 82 | return ENS_NETS.includes(chainId) 83 | } 84 | 85 | // get deployed address for a contract from its networks object and current network id or null 86 | export async function getNetworkAddress(networks) { 87 | if (!networks[chainId] || !networks[chainId].address) return null 88 | return networks[chainId].address 89 | } 90 | 91 | export function getProvider() { 92 | return provider 93 | } 94 | 95 | export function getWallet() { 96 | return userWallet 97 | } 98 | 99 | export async function getWalletAddress() { 100 | const addr = userWallet && await userWallet.getAddress() 101 | return addr 102 | } 103 | 104 | export function ready() { 105 | return !!provider && !!userWallet 106 | } 107 | 108 | export async function startProviderWatcher() { 109 | // this should only be run when a ethereum provider is detected and set at the ethereum value above 110 | async function updateProvider() { 111 | try { 112 | ethereum = getEthereum() 113 | if (!ethereum) return 114 | // set ethers provider 115 | provider = new providers.Web3Provider(ethereum) 116 | initialized = true 117 | 118 | // this is modeled after EIP-1193 example provided by MetaMask for clarity and consistency 119 | // but works for all EIP-1193 compatible ethereum providers 120 | // https://docs.metamask.io/guide/ethereum-provider.html#using-the-provider 121 | 122 | /**********************************************************/ 123 | /* Handle chain (network) and chainChanged (per EIP-1193) */ 124 | /**********************************************************/ 125 | 126 | // Normally, we would recommend the 'eth_chainId' RPC method, but it currently 127 | // returns incorrectly formatted chain ID values. 128 | chainId = ethereum.chainId 129 | 130 | ethereum.on('chainChanged', handleChainChanged) 131 | 132 | /***********************************************************/ 133 | /* Handle user accounts and accountsChanged (per EIP-1193) */ 134 | /***********************************************************/ 135 | 136 | const accounts = await ethereum.request({ method: 'eth_accounts' }) 137 | await handleAccountsChanged(accounts) 138 | // Note that this event is emitted on page load. 139 | // If the array of accounts is non-empty, you're already 140 | // connected. 141 | ethereum.on('accountsChanged', handleAccountsChanged) 142 | } catch (err) { 143 | // Some unexpected error. 144 | // For backwards compatibility reasons, if no accounts are available, 145 | // eth_accounts will return an empty array. 146 | console.error('Error requesting ethereum accounts', err) 147 | event.$emit(EVENT_CHANNEL, MSGS.NO_WALLET) 148 | } 149 | } 150 | 151 | function checkProvider() { 152 | // handle changes of availability of ethereum provider 153 | if (ethereum && !ethereumOk()) { 154 | ethereum = null 155 | provider = null 156 | chainId = null 157 | currentAccount = null 158 | userWallet = null 159 | event.$emit(EVENT_CHANNEL, MSGS.NOT_READY) 160 | } else if (!ethereum && ethereumOk()) { 161 | updateProvider() 162 | } 163 | } 164 | 165 | // kick it off now 166 | checkProvider() 167 | // and set interval 168 | providerInterval = setInterval(checkProvider, PROVIDER_CHECK_MS) 169 | } 170 | 171 | function handleChainChanged(_chainId) { 172 | // We recommend reloading the page, unless you must do otherwise 173 | console.log('Ethereum chain changed. Reloading as recommended.') 174 | chainId = _chainId 175 | alert('Ethereum chain has changed. We will reload the page as recommended.') 176 | window.location.reload() 177 | } 178 | 179 | // For now, 'eth_accounts' will continue to always return an array 180 | function handleAccountsChanged(accounts) { 181 | if (accounts.length === 0) { 182 | // MetaMask is locked or the user has not connected any accounts 183 | console.log('No ethereum accounts available') 184 | event.$emit(EVENT_CHANNEL, MSGS.NO_WALLET) 185 | } else if (accounts[0] !== currentAccount) { 186 | currentAccount = accounts[0] 187 | userWallet = provider && provider.getSigner(currentAccount) 188 | event.$emit(EVENT_CHANNEL, MSGS.ACCOUNT_CHANGED) 189 | } 190 | } 191 | 192 | /*********************************************/ 193 | /* Access the user's accounts (per EIP-1102) */ 194 | /*********************************************/ 195 | 196 | // You should only attempt to request the user's accounts in response to user 197 | // interaction, such as a button click. 198 | // Otherwise, you popup-spam the user like it's 1999. 199 | // If you fail to retrieve the user's account(s), you should encourage the user 200 | // to initiate the attempt. 201 | // document.getElementById('connectButton', connect) 202 | 203 | export async function connect() { 204 | try { 205 | if (!ethereum) return event.$emit(EVENT_CHANNEL, MSGS.NOT_CONNECTED) 206 | const accounts = await ethereum.request({ method: 'eth_requestAccounts' }) 207 | await handleAccountsChanged(accounts) 208 | await event.$emit(EVENT_CHANNEL, MSGS.ACCOUNT_CHANGED) 209 | } catch (err) { 210 | if (err.code === 4001) { 211 | // EIP-1193 userRejectedRequest error 212 | // If this happens, the user rejected the connection request. 213 | console.log('Please connect to Ethereum wallet') 214 | event.$emit(EVENT_CHANNEL, MSGS.NOT_READY, err) 215 | } else { 216 | console.error('Error requesting Ethereum connection/accounts', err) 217 | event.$emit(EVENT_CHANNEL, MSGS.NOT_READY, err) 218 | } 219 | } 220 | } 221 | 222 | // stop interval looking for ethereum provider changes 223 | export async function stopWatchProvider() { 224 | if (providerInterval) clearInterval(providerInterval) 225 | providerInterval = null 226 | } 227 | 228 | // start ethereum provider checker 229 | startProviderWatcher() 230 | 231 | export default { 232 | connect, 233 | ethereumOk, 234 | getNetName, 235 | hasEns, 236 | getProvider, 237 | getWallet, 238 | getWalletAddress, 239 | getNetworkAddress, 240 | ready 241 | } 242 | -------------------------------------------------------------------------------- /generator/templates/module/getters.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default {} 3 | -------------------------------------------------------------------------------- /generator/templates/module/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import getters from './getters' 3 | import actions from './actions' 4 | import mutations from './mutations' 5 | 6 | const state = () => ({ 7 | initialized: false, 8 | connected: false, 9 | error: null, 10 | // user is ens or address 11 | user: '', 12 | address: '', 13 | network: '', 14 | ens: null 15 | }) 16 | 17 | export default { 18 | namespaced: true, 19 | state, 20 | getters, 21 | actions, 22 | mutations 23 | } 24 | -------------------------------------------------------------------------------- /generator/templates/module/mutations.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | initialized: function (state, value) { 4 | state.initialized = value 5 | }, 6 | connected: function (state, value) { 7 | state.connected = value 8 | }, 9 | error: function (state, value) { 10 | state.error = value 11 | }, 12 | user: function (state, value) { 13 | state.user = value 14 | }, 15 | address: function (state, value) { 16 | state.address = value 17 | }, 18 | network: function (state, value) { 19 | state.network = value 20 | }, 21 | ens: function (state, value) { 22 | state.ens = value 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = (api, options) => {} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli-plugin-ethers", 3 | "description": "Ethereum ethers.js vuex store module generator plugin for vue-cli 3. Build dapps!", 4 | "version": "0.2.3", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/bmiller59/vue-cli-plugin-ethers" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/bmiller59/vue-cli-plugin-ethers/issues" 12 | }, 13 | "homepage": "https://github.com/bmiller59/vue-cli-plugin-ethers#readme", 14 | "author": "Brendan Miller ", 15 | "license": "MIT", 16 | "publishConfig": { 17 | "access": "public" 18 | }, 19 | "keywords": [ 20 | "ethereum", 21 | "ethers", 22 | "web3", 23 | "vuex", 24 | "store", 25 | "vue", 26 | "cli", 27 | "plugin", 28 | "dapp" 29 | ], 30 | "dependencies": {}, 31 | "devDependencies": {} 32 | } 33 | -------------------------------------------------------------------------------- /prompts.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 3 | ] 4 | --------------------------------------------------------------------------------