├── .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 |
--------------------------------------------------------------------------------