diff --git a/package.json b/package.json index 3d5db05..4ababd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ezerous/drizzle", - "version": "0.1.0", + "version": "0.2.0", "description": "A reactive data-store for web3 and smart contracts.", "license": "MIT", "author": "Ezerous ", diff --git a/src/Drizzle.js b/src/Drizzle.js index 02c50ec..b52eeed 100644 --- a/src/Drizzle.js +++ b/src/Drizzle.js @@ -6,7 +6,7 @@ import * as ContractActions from './contracts/constants' import * as DrizzleActions from './drizzleStatus/drizzleActions' // Load as promise so that async Drizzle initialization can still resolve -var isEnvReadyPromise = new Promise((resolve, reject) => { +const isEnvReadyPromise = new Promise((resolve) => { const hasNavigator = typeof navigator !== 'undefined' const hasWindow = typeof window !== 'undefined' const hasDocument = typeof document !== 'undefined' @@ -81,12 +81,6 @@ class Drizzle { events ) - if (this.contracts[drizzleContract.contractName]) { - throw new Error( - `Contract already exists: ${drizzleContract.contractName}` - ) - } - this.store.dispatch({ type: ContractActions.CONTRACT_INITIALIZING, contractConfig }) this.contracts[drizzleContract.contractName] = drizzleContract diff --git a/src/accountBalances/accountBalancesMiddleware.js b/src/accountBalances/accountBalancesMiddleware.js index dcf3fc0..d784474 100644 --- a/src/accountBalances/accountBalancesMiddleware.js +++ b/src/accountBalances/accountBalancesMiddleware.js @@ -1,4 +1,4 @@ -import { WEB3_INITIALIZED } from '../web3/constants' +import { WEB3_INITIALIZED } from '../web3/web3Actions' import { accountBalancesFetching } from './accountBalancesActions' import { ACCOUNTS_FETCHED } from '../accounts/accountsActions' diff --git a/src/accountBalances/accountBalancesSaga.js b/src/accountBalances/accountBalancesSaga.js index 6fa67ea..377e28c 100644 --- a/src/accountBalances/accountBalancesSaga.js +++ b/src/accountBalances/accountBalancesSaga.js @@ -1,7 +1,7 @@ import { call, put, select, takeLatest } from 'redux-saga/effects' import { - ACCOUNT_BALANCES_FAILED, ACCOUNT_BALANCE_FETCHED, + ACCOUNT_BALANCES_FAILED, ACCOUNT_BALANCES_FETCHED, ACCOUNT_BALANCES_FETCHING } from './accountBalancesActions' diff --git a/src/accounts/accountsMiddleware.js b/src/accounts/accountsMiddleware.js index 73c3151..4c9128f 100644 --- a/src/accounts/accountsMiddleware.js +++ b/src/accounts/accountsMiddleware.js @@ -1,14 +1,14 @@ -import { WEB3_INITIALIZED } from '../web3/constants' +import { WEB3_INITIALIZED } from '../web3/web3Actions' import { accountsFetched, accountsListening } from './accountsActions' -export const accountsMiddleware = () => store => next => action => { +export const accountsMiddleware = web3 => store => next => action => { const { type } = action if (type === WEB3_INITIALIZED) { if(!window.ethereum) console.warn('No Metamask detected, not subscribed to account changes!') else { - const { web3 } = action; + web3 = action.web3; window.ethereum.on('accountsChanged', accounts => { // For some reason accounts here are returned with lowercase letters, so we need to patch them let patchedAccounts = Array.from(accounts); diff --git a/src/blocks/blocksSaga.js b/src/blocks/blocksSaga.js index 8292263..091e7bc 100644 --- a/src/blocks/blocksSaga.js +++ b/src/blocks/blocksSaga.js @@ -1,5 +1,5 @@ import { END, eventChannel } from 'redux-saga' -import { call, put, take, takeEvery, takeLatest, all } from 'redux-saga/effects' +import { all, call, put, take, takeEvery, takeLatest } from 'redux-saga/effects' import * as BlocksActions from './blockActions' /* diff --git a/src/contractStateUtils.js b/src/contractStateUtils.js index 8457648..6ed534a 100644 --- a/src/contractStateUtils.js +++ b/src/contractStateUtils.js @@ -22,6 +22,7 @@ export const generateContractInitialState = contractConfig => { return { initialized: false, synced: false, + deployed: true, ...objectOfConstants } } diff --git a/src/contracts/constants.js b/src/contracts/constants.js index c98ab27..54d4f54 100644 --- a/src/contracts/constants.js +++ b/src/contracts/constants.js @@ -4,6 +4,7 @@ export const EVENT_ERROR = 'EVENT_ERROR' export const LISTEN_FOR_EVENT = 'LISTEN_FOR_EVENT' export const CONTRACT_INITIALIZING = 'CONTRACT_INITIALIZING' export const CONTRACT_INITIALIZED = 'CONTRACT_INITIALIZED' +export const CONTRACT_NOT_DEPLOYED = 'CONTRACT_NOT_DEPLOYED' export const GOT_CONTRACT_VAR = 'GOT_CONTRACT_VAR' export const DELETE_CONTRACT = 'DELETE_CONTRACT' export const CONTRACT_SYNCING = 'CONTRACT_SYNCING' diff --git a/src/contracts/contractsReducer.js b/src/contracts/contractsReducer.js index 8af3006..540ded4 100644 --- a/src/contracts/contractsReducer.js +++ b/src/contracts/contractsReducer.js @@ -29,6 +29,17 @@ const contractsReducer = (state = initialState, action) => { } } + // Contract not found on the current network + if (action.type === ContractActions.CONTRACT_NOT_DEPLOYED) { + return { + ...state, + [action.name]: { + ...state[action.name], + deployed: false + } + } + } + if (action.type === ContractActions.DELETE_CONTRACT) { const { [action.contractName]: omitted, ...rest } = state return rest diff --git a/src/contracts/contractsSaga.js b/src/contracts/contractsSaga.js index 1ba4395..d50d342 100644 --- a/src/contracts/contractsSaga.js +++ b/src/contracts/contractsSaga.js @@ -199,7 +199,7 @@ function * callCallContractFn ({ } catch (error) { console.error(error) - var errorArgs = { + const errorArgs = { name: contract.contractName, variable: contract.abi[fnIndex].name, argsHash: argsHash, @@ -230,8 +230,8 @@ function * callSyncContract (action) { delete contractFnsState.events // Iterate over functions and hashes - for (var fnName in contractFnsState) { - for (var argsHash in contractFnsState[fnName]) { + for (let fnName in contractFnsState) { + for (let argsHash in contractFnsState[fnName]) { const fnIndex = contractFnsState[fnName][argsHash].fnIndex const args = contractFnsState[fnName][argsHash].args @@ -271,6 +271,18 @@ function isSendOrCallOptions (options) { return false } +export function * isContractDeployed ({ web3, contractConfig }) { + const networkId = yield call(web3.eth.net.getId); + if(contractConfig.networks[networkId]){ + const contractAddress = contractConfig.networks[networkId].address; + + const fetchedByteCode = yield call(web3.eth.getCode, contractAddress); + if(fetchedByteCode === contractConfig.deployedBytecode) + return true; + } + return false; +} + function * contractsSaga () { yield takeEvery(ContractActions.SEND_CONTRACT_TX, callSendContractTx) yield takeEvery(ContractActions.CALL_CONTRACT_FN, callCallContractFn) diff --git a/src/drizzle-middleware.js b/src/drizzle-middleware.js index 0718af2..71d96f4 100644 --- a/src/drizzle-middleware.js +++ b/src/drizzle-middleware.js @@ -1,6 +1,7 @@ import * as DrizzleActions from './drizzleStatus/drizzleActions' import * as ContractActions from './contracts/constants' import { ACCOUNTS_FETCHED } from './accounts/accountsActions' +import { NETWORK_ID_CHANGED } from './web3/web3Actions' export const drizzleMiddleware = drizzleInstance => store => next => action => { const { type } = action @@ -9,6 +10,14 @@ export const drizzleMiddleware = drizzleInstance => store => next => action => { drizzleInstance = action.drizzle } + if (type === NETWORK_ID_CHANGED) { + store.dispatch({ + type: DrizzleActions.DRIZZLE_INITIALIZING, + drizzle: drizzleInstance, + options: drizzleInstance.options + }) + } + if ( type === ACCOUNTS_FETCHED && drizzleInstance && diff --git a/src/drizzleStatus/drizzleStatusSaga.js b/src/drizzleStatus/drizzleStatusSaga.js index db28571..7879f5e 100644 --- a/src/drizzleStatus/drizzleStatusSaga.js +++ b/src/drizzleStatus/drizzleStatusSaga.js @@ -1,13 +1,15 @@ import { call, put, takeLatest } from 'redux-saga/effects' // Initialization Functions -import { initializeWeb3, getNetworkId } from '../web3/web3Saga' +import { getNetworkId, initializeWeb3 } from '../web3/web3Saga' import { getAccounts } from '../accounts/accountsSaga' import { getAccountBalances } from '../accountBalances/accountBalancesSaga' import * as DrizzleActions from './drizzleActions' import * as BlocksActions from '../blocks/blockActions' -import { NETWORK_IDS, NETWORK_MISMATCH } from '../web3/constants' +import { NETWORK_IDS, NETWORK_MISMATCH } from '../web3/web3Actions' +import { CONTRACT_NOT_DEPLOYED } from '../contracts/constants' +import { isContractDeployed } from '../contracts/contractsSaga' export function * initializeDrizzle (action) { try { @@ -35,15 +37,19 @@ export function * initializeDrizzle (action) { yield call(getAccountBalances, { web3 }) // Instantiate contracts passed through via options. - for (var i = 0; i < options.contracts.length; i++) { - var contractConfig = options.contracts[i] - var events = [] - var contractName = contractConfig.contractName - + for (let i = 0; i < options.contracts.length; i++) { + const contractConfig = options.contracts[i] + let events = [] + const contractName = contractConfig.contractName; if (contractName in options.events) { events = options.events[contractName] } + if(!(yield call(isContractDeployed, { web3, contractConfig }))){ + yield put({ type: CONTRACT_NOT_DEPLOYED, name: contractName }) + throw `Contract ${contractName} not deployed on this network` + } + yield call([drizzle, drizzle.addContract], contractConfig, events) } diff --git a/src/generateStore.js b/src/generateStore.js index 7d12a67..603b364 100644 --- a/src/generateStore.js +++ b/src/generateStore.js @@ -1,5 +1,5 @@ import { all, fork } from 'redux-saga/effects' -import { createStore, applyMiddleware, compose, combineReducers } from 'redux' +import { applyMiddleware, combineReducers, compose, createStore } from 'redux' import createSagaMiddleware from 'redux-saga' import drizzleSagas from './rootSaga' import drizzleReducers from './reducer' diff --git a/src/index.js b/src/index.js index 83a3eb3..1f48a64 100644 --- a/src/index.js +++ b/src/index.js @@ -7,53 +7,27 @@ import * as EventActions from './contracts/constants' import * as AccountActions from './accounts/accountsActions' // Reducers -import accountsReducer from './accounts/accountsReducer' -import accountBalancesReducer from './accountBalances/accountBalancesReducer' -import blocksReducer from './blocks/blocksReducer' -import contractsReducer from './contracts/contractsReducer' -import drizzleStatusReducer from './drizzleStatus/drizzleStatusReducer' -import transactionsReducer from './transactions/transactionsReducer' -import transactionStackReducer from './transactions/transactionStackReducer' -import web3Reducer from './web3/web3Reducer' +import drizzleReducers from './reducer' // Middleware import drizzleMiddleware from './drizzle-middleware' import accountsMiddleware from './accounts/accountsMiddleware' import accountBalancesMiddleware from './accountBalances/accountBalancesMiddleware' +import web3Middleware from './web3/web3Middleware' // Sagas -import accountBalancesSaga from './accountBalances/accountBalancesSaga' -import blocksSaga from './blocks/blocksSaga' -import contractsSaga from './contracts/contractsSaga' -import drizzleStatusSaga from './drizzleStatus/drizzleStatusSaga' - -const drizzleReducers = { - accounts: accountsReducer, - accountBalances: accountBalancesReducer, - contracts: contractsReducer, - currentBlock: blocksReducer, - drizzleStatus: drizzleStatusReducer, - transactions: transactionsReducer, - transactionStack: transactionStackReducer, - web3: web3Reducer -} +import drizzleSagas from './rootSaga' const drizzleMiddlewares = [ drizzleMiddleware, accountsMiddleware, - accountBalancesMiddleware -] - -const drizzleSagas = [ - accountBalancesSaga, - blocksSaga, - contractsSaga, - drizzleStatusSaga + accountBalancesMiddleware, + web3Middleware ] const drizzleActions = { - AccountActions, - EventActions + account: AccountActions, + event: EventActions } export { diff --git a/src/mergeOptions.js b/src/mergeOptions.js index f30bd87..10ddeae 100644 --- a/src/mergeOptions.js +++ b/src/mergeOptions.js @@ -1,5 +1,5 @@ const merge = require('deepmerge'); -import isPlainObject from 'is-plain-object'; +import isPlainObject from 'is-plain-object' export default function (defaultOptions, newOptions) { return merge(defaultOptions, newOptions, { diff --git a/src/web3/constants.js b/src/web3/web3Actions.js similarity index 65% rename from src/web3/constants.js rename to src/web3/web3Actions.js index 2943c92..2175bdc 100644 --- a/src/web3/constants.js +++ b/src/web3/web3Actions.js @@ -3,7 +3,9 @@ export const WEB3_INITIALIZED = 'WEB3_INITIALIZED' export const WEB3_FAILED = 'WEB3_FAILED' export const WEB3_USER_DENIED = 'WEB3_USER_DENIED' +export const NETWORK_ID_FETCHING = 'NETWORK_ID_FETCHING' export const NETWORK_ID_FETCHED = 'NETWORK_ID_FETCHED' +export const NETWORK_ID_CHANGED = 'NETWORK_ID_CHANGED' export const NETWORK_ID_FAILED = 'NETWORK_ID_FAILED' export const NETWORK_MISMATCH = 'NETWORK_MISMATCH' @@ -15,3 +17,11 @@ export const NETWORK_IDS = { kovan: 42, ganache: 5777 } + +export function networkIdChanged (web3, networkId) { + return { + type: NETWORK_ID_CHANGED, + web3, + networkId + } +} diff --git a/src/web3/web3Middleware.js b/src/web3/web3Middleware.js new file mode 100644 index 0000000..2c4557c --- /dev/null +++ b/src/web3/web3Middleware.js @@ -0,0 +1,23 @@ +import { networkIdChanged, WEB3_INITIALIZED } from './web3Actions' + +export const web3Middleware = web3 => store => next => action => { + const { type } = action + + if (type === WEB3_INITIALIZED) { + if(!window.ethereum) + console.warn('No Metamask detected, not subscribed to network changes!') + else { + web3 = action.web3; + window.ethereum.on('networkChanged', (networkId) => { + // Warning: 'networkChanged' is deprecated (EIP-1193) + const storedNetworkId = store.getState().web3.networkId; + if(storedNetworkId && networkId !== storedNetworkId) + store.dispatch(networkIdChanged(web3, networkId)); + }); + } + } + return next(action) +} + +const initializedMiddleware = web3Middleware(undefined) +export default initializedMiddleware diff --git a/src/web3/web3Reducer.js b/src/web3/web3Reducer.js index e29677e..b81c50a 100644 --- a/src/web3/web3Reducer.js +++ b/src/web3/web3Reducer.js @@ -1,4 +1,4 @@ -import * as Action from './constants' +import * as Action from './web3Actions' const initialState = { status: '' @@ -33,7 +33,8 @@ const web3Reducer = (state = initialState, action) => { } } - if (action.type === Action.NETWORK_ID_FETCHED) { + if (action.type === Action.NETWORK_ID_FETCHED + || action.type === Action.NETWORK_ID_CHANGED) { return { ...state, networkId: action.networkId diff --git a/src/web3/web3Saga.js b/src/web3/web3Saga.js index cb13747..d3fc3ee 100644 --- a/src/web3/web3Saga.js +++ b/src/web3/web3Saga.js @@ -1,12 +1,11 @@ import { call, put } from 'redux-saga/effects' -import * as Action from './constants' +import * as Action from './web3Actions' const Web3 = require('web3'); /* * Initialization */ - export function * initializeWeb3 (options) { try { let web3 = {} @@ -35,7 +34,6 @@ export function * initializeWeb3 (options) { } catch (error) { console.error(error) yield put({ type: Action.WEB3_FAILED }) - return } } else if (typeof window.web3 !== 'undefined') { // Checking if Web3 has been injected by the browser (Mist/MetaMask) @@ -71,20 +69,19 @@ export function * initializeWeb3 (options) { } /* - * Network ID + * Fetch Network ID */ - export function * getNetworkId ({ web3 }) { try { - const networkId = yield call(web3.eth.net.getId) + const networkId = yield call(web3.eth.net.getId); - yield put({ type: Action.NETWORK_ID_FETCHED, networkId }) + yield put({ type: Action.NETWORK_ID_FETCHED, networkId }); return networkId } catch (error) { - yield put({ type: Action.NETWORK_ID_FAILED, error }) + yield put({ type: Action.NETWORK_ID_FAILED, error }); - console.error('Error fetching network ID:') - console.error(error) + console.error('Error fetching network ID:'); + console.error(error); } }