Merge pull request #116 from gnosis/web3-providers-refactor

Web3 providers refactoring
This commit is contained in:
Germán Martínez 2019-06-07 10:47:32 +02:00 committed by GitHub
commit eb8b3585fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 90 additions and 68 deletions

View File

@ -0,0 +1 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" fill="#fff"><path d="m6.72043466.05975844h-6.62356495v11.84970376h6.62356495z" fill="#fff" fill-rule="evenodd"/></mask><g fill="#4a5579" fill-rule="evenodd"><path d="m4.14977665 6.07551536-1.71737342-1.70732942c-.154797.20785847-.24831026.46589514-.24831026.74441596 0 .67391239.54853704 1.22126772 1.22091168 1.22126772.27926965 0 .53821724-.09336958.744772-.25835426"/><path d="m1.87126776 2.19027617c1.03982725-1.02106347 2.40224018-1.57576779 3.8633547-1.57576779h.00917804c.01609919 0 .03219839.00091234.04814712.00121646v5.5467391zm3.85282252-2.13062417c-1.69583171 0-3.2740039.69414069-4.43103929 1.93158142l-.18657008.19904275 4.68546671 4.75589009v.85273625l-.01850655.01870302-1.20548342-1.2283173c-.55925885.37618928-1.27484536.48551834-1.95071051.24739691-1.13777654-.41618029-1.72637503-1.67445284-1.31411535-2.81366468.05852884-.17821093.14669825-.3371106.24449708-.48536628l-.51938702-.5253573-.09794929.16847927c-.5392477.89211904-.83339649 1.92230593-.83339649 2.98260013-.00947896 3.16111517 2.53900808 5.74578182 5.66631374 5.74578182l.95722485.0003041v-11.84889785z" mask="url(#a)" transform="translate(.218409)"/><path d="m11.7375061 6.11561396c0-2.64879063-2.15538824-4.8037754-4.80500459-4.80515818v.57492892c2.33248021.00122914 4.22991669 1.89824874 4.22991669 4.23022926 0 2.33136596-1.89743648 4.22869284-4.22991669 4.22992194v.574929c2.64961635-.0013828 4.80500459-2.15636759 4.80500459-4.80485094"/><path d="m8.02004901 6.01820975-.22912891 1.40770636h1.1220214l-.2288201-1.40770636c.25522242-.12241582.43247309-.37950419.43247309-.67820486 0-.41582039-.34214937-.75340965-.76443209-.75340965-.42197392 0-.76443209.33758926-.76443209.75340965-.00046319.2990033.17647867.55578904.4323187.67820486"/><path d="m7.75184322 2.83111882c.28114484.0716844.38948776-.34809944.1084868-.42035731-.28100096-.07197114-.38934388.34809944-.1084868.42035731"/><path d="m8.97850178 3.44035856c.21945107.18400118.4955759-.14470408.27612484-.3285634-.21945107-.18371744-.49543387.14484596-.27612484.3285634"/><path d="m9.79686881 4.68163059c.12643597.25912811.51314779.0686432.38671179-.19048491-.1261512-.25841307-.51314776-.06821418-.38671179.19048491"/><path d="m7.75184322 9.3998634c.28114484-.07200813.38948776.34827835.1084868.42028648s-.38934388-.34799147-.1084868-.42028648"/><path d="m8.97850178 8.5721264c.21945107-.18396063.4955759.14481403.27612484.32849098-.21945107.18396062-.49543387-.14481403-.27612484-.32849098"/><path d="m9.79686881 7.54913594c.12643597-.25888516.51314779-.06847377.38671179.19069729-.1261512.25831336-.51314776.06818787-.38671179-.19069729"/><path d="m10.208641 6.11535204c0 .29135677.4368186.29135677.4368186 0 0-.29106802-.4368186-.29106802-.4368186 0"/></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 557 B

View File

@ -21,7 +21,8 @@ import { shortVersionOf } from '~/logic/wallets/ethAddresses'
import { getEtherScanLink } from '~/logic/wallets/getWeb3'
import CircleDot from '~/components/Header/component/CircleDot'
const metamask = require('../../assets/metamask.svg')
const metamaskIcon = require('../../assets/metamask-icon.svg')
const safeIcon = require('../../assets/gnosis-safe-icon.svg')
const dot = require('../../assets/dotRinkeby.svg')
type Props = {
@ -132,7 +133,6 @@ const UserDetails = ({
<Row className={classes.details}>
<Paragraph noMargin align="right" className={classes.labels}>
Status
{' '}
</Paragraph>
<Spacer />
<Dot className={classNames(classes.dot, connected ? classes.connected : classes.warning)} />
@ -144,10 +144,12 @@ const UserDetails = ({
<Row className={classes.details}>
<Paragraph noMargin align="right" className={classes.labels}>
Client
{' '}
</Paragraph>
<Spacer />
<Img className={classes.logo} src={metamask} height={14} alt="Metamask client" />
{provider === 'safe'
? <Img className={classes.logo} src={safeIcon} height={14} alt="Safe client" />
: <Img className={classes.logo} src={metamaskIcon} height={14} alt="Metamask client" />
}
<Paragraph noMargin align="right" weight="bolder" className={classes.labels}>
{upperFirst(provider)}
</Paragraph>
@ -156,7 +158,6 @@ const UserDetails = ({
<Row className={classes.details}>
<Paragraph noMargin align="right" className={classes.labels}>
Network
{' '}
</Paragraph>
<Spacer />
<Img className={classes.logo} src={dot} height={14} alt="Network" />

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux'
import { logComponentStack, type Info } from '~/utils/logBoundaries'
import { SharedSnackbarConsumer, type Variant } from '~/components/SharedSnackBar/Context'
import { WALLET_ERROR_MSG } from '~/logic/wallets/store/actions'
import { getProviderInfo } from '~/logic/wallets/getWeb3'
import ProviderAccesible from './component/ProviderInfo/ProviderAccesible'
import UserDetails from './component/ProviderDetails/UserDetails'
import ProviderDisconnected from './component/ProviderInfo/ProviderDisconnected'
@ -30,16 +31,35 @@ class HeaderComponent extends React.PureComponent<Props, State> {
this.onConnect()
}
componentDidCatch(error: Error, info: Info) {
const { openSnackbar } = this.props
this.setState({ hasError: true })
openSnackbar(WALLET_ERROR_MSG, 'error')
logComponentStack(error, info)
}
onDisconnect = () => {
const { removeProvider, openSnackbar } = this.props
clearInterval(this.providerListener)
removeProvider(openSnackbar)
}
onConnect = () => {
onConnect = async () => {
const { fetchProvider, openSnackbar } = this.props
fetchProvider(openSnackbar)
clearInterval(this.providerListener)
let currentProvider: ProviderProps = await getProviderInfo()
fetchProvider(currentProvider, openSnackbar)
this.providerListener = setInterval(async () => {
const newProvider: ProviderProps = await getProviderInfo()
if (JSON.stringify(currentProvider) !== JSON.stringify(newProvider)) {
fetchProvider(newProvider, openSnackbar)
}
currentProvider = newProvider
}, 2000)
}
getProviderInfoBased = () => {
@ -76,14 +96,6 @@ class HeaderComponent extends React.PureComponent<Props, State> {
)
}
componentDidCatch(error: Error, info: Info) {
const { openSnackbar } = this.props
this.setState({ hasError: true })
openSnackbar(WALLET_ERROR_MSG, 'error')
logComponentStack(error, info)
}
render() {
const info = this.getProviderInfoBased()
const details = this.getProviderDetailsBased()

View File

@ -14,7 +14,7 @@ const calculateStyleFrom = (color?: string, margin?: Size) => ({
type Props = {
margin?: Size,
color?: string,
style?: Object
style?: Object,
}
const Hairline = ({ margin, color, style }: Props) => {

View File

@ -75,7 +75,7 @@ export const deploySafeContract = async (safeAccounts: string[], numConfirmation
export const getGnosisSafeInstanceAt = async (safeAddress: string) => {
const web3 = getWeb3()
const GnosisSafe = getGnosisSafeContract(web3)
const GnosisSafe = await getGnosisSafeContract(web3)
const gnosisSafe = await GnosisSafe.at(safeAddress)
return gnosisSafe

View File

@ -15,9 +15,9 @@ const createStandardTokenContract = async () => {
const web3 = getWeb3()
const erc20Token = await contract(StandardToken)
erc20Token.setProvider(web3.currentProvider)
return erc20Token
}
const createHumanFriendlyTokenContract = async () => {
const web3 = getWeb3()
const humanErc20Token = await contract(HumanFriendlyToken)

View File

@ -9,7 +9,6 @@ import saveTokens from './saveTokens'
const loadActiveTokens = () => async (dispatch: ReduxDispatch<GlobalState>) => {
try {
const tokens: Map<string, TokenProps> = await getActiveTokens()
const tokenRecordsList: List<Token> = List(
Object.values(tokens).map((token: TokenProps): Token => makeToken(token)),
)

View File

@ -12,6 +12,7 @@ export const ETHEREUM_NETWORK = {
}
export const WALLET_PROVIDER = {
SAFE: 'SAFE',
METAMASK: 'METAMASK',
PARITY: 'PARITY',
REMOTE: 'REMOTE',
@ -36,12 +37,23 @@ export const openTxInEtherScan = (tx: string, network: string) => `https://${net
export const getEtherScanLink = (address: string, network: string) => `https://${network}.etherscan.io/address/${address}`
let web3
export const getWeb3 = () => web3 || new Web3(window.web3.currentProvider)
export const getWeb3 = () => web3 || (window.web3 && new Web3(window.web3.currentProvider)) || (window.ethereum && new Web3(window.ethereum))
const isMetamask: Function = (web3Provider): boolean => {
const isMetamaskConstructor = web3Provider.currentProvider.constructor.name === 'MetamaskInpageProvider'
const getProviderName: Function = (web3Provider): boolean => {
let name
return isMetamaskConstructor || web3Provider.currentProvider.isMetaMask
switch (web3Provider.currentProvider.constructor.name) {
case 'SafeWeb3Provider':
name = WALLET_PROVIDER.SAFE
break
case 'MetamaskInpageProvider':
name = WALLET_PROVIDER.METAMASK
break
default:
name = 'UNKNOWN'
}
return name
}
const getAccountFrom: Function = async (web3Provider): Promise<string | null> => {
@ -57,7 +69,14 @@ const getNetworkIdFrom = async (web3Provider) => {
}
export const getProviderInfo: Function = async (): Promise<ProviderProps> => {
if (typeof window.web3 === 'undefined') {
let web3Provider
if (window.ethereum) {
web3Provider = window.ethereum
await web3Provider.enable()
} else if (window.web3) {
web3Provider = window.web3.currentProvider
} else {
return {
name: '',
available: false,
@ -67,15 +86,9 @@ export const getProviderInfo: Function = async (): Promise<ProviderProps> => {
}
}
// Use MetaMask's provider.
web3 = new Web3(window.web3.currentProvider)
web3 = new Web3(web3Provider)
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line
console.log('Injected web3 detected.')
}
const name = isMetamask(window.web3) ? WALLET_PROVIDER.METAMASK : 'UNKNOWN'
const name = getProviderName(web3)
const account = await getAccountFrom(web3)
const network = await getNetworkIdFrom(web3)

View File

@ -1,6 +1,6 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'redux'
import { getProviderInfo, ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
import { ETHEREUM_NETWORK_IDS, ETHEREUM_NETWORK } from '~/logic/wallets/getWeb3'
import type { ProviderProps } from '~/logic/wallets/store/model/provider'
import { makeProvider } from '~/logic/wallets/store/model/provider'
import addProvider from './addProvider'
@ -44,10 +44,7 @@ const handleProviderNotification = (openSnackbar: Function, provider: ProviderPr
openSnackbar(msg, variant)
}
export default (openSnackbar: Function) => async (dispatch: ReduxDispatch<*>) => {
const provider: ProviderProps = await getProviderInfo()
export default (provider: ProviderProps, openSnackbar: Function) => (dispatch: ReduxDispatch<*>) => {
handleProviderNotification(openSnackbar, provider)
processProviderResponse(dispatch, provider)
}

View File

@ -20,9 +20,9 @@ const providerReducerTests = () => {
store = createStore(reducers, compose(...enhancers))
})
it('reducer should return default Provider record when no Metamask is loaded', () => {
it('reducer should return default Provider record when no provider is loaded', () => {
// GIVEN
const emptyResponse: ProviderProps = {
const emptyProvider: ProviderProps = {
name: '',
loaded: false,
available: false,
@ -31,17 +31,17 @@ const providerReducerTests = () => {
}
// WHEN
processProviderResponse(store.dispatch, emptyResponse)
processProviderResponse(store.dispatch, emptyProvider)
const provider = store.getState()[PROVIDER_REDUCER_ID]
// THEN
expect(makeProvider(emptyResponse)).toEqual(provider)
expect(makeProvider(emptyProvider)).toEqual(provider)
})
it('reducer should return avaiable with its default value when is loaded but not available', () => {
// GIVEN
const metamaskLoaded: ProviderProps = {
name: 'METAMASK',
const providerLoaded: ProviderProps = {
name: 'SAFE',
loaded: true,
available: false,
account: '',
@ -49,17 +49,17 @@ const providerReducerTests = () => {
}
// WHEN
processProviderResponse(store.dispatch, metamaskLoaded)
processProviderResponse(store.dispatch, providerLoaded)
const provider = store.getState()[PROVIDER_REDUCER_ID]
// THEN
expect(makeProvider(metamaskLoaded)).toEqual(provider)
expect(makeProvider(providerLoaded)).toEqual(provider)
})
it('reducer should return metamask provider when it is loaded and available', () => {
it('reducer should return provider when it is loaded and available', () => {
// GIVEN
const metamask: ProviderProps = {
name: 'METAMASK',
const providerLoaded: ProviderProps = {
name: 'SAFE',
loaded: true,
available: true,
account: '',
@ -67,11 +67,11 @@ const providerReducerTests = () => {
}
// WHEN
processProviderResponse(store.dispatch, metamask)
processProviderResponse(store.dispatch, providerLoaded)
const provider = store.getState()[PROVIDER_REDUCER_ID]
// THEN
expect(makeProvider(metamask)).toEqual(provider)
expect(makeProvider(providerLoaded)).toEqual(provider)
})
})
}

View File

@ -25,13 +25,9 @@ export const addSafe = createAction<string, Function, ActionReturn>(
}),
)
const saveSafe = (
name: string,
address: string,
threshold: number,
ownersName: string[],
ownersAddress: string[],
) => (dispatch: ReduxDispatch<GlobalState>) => {
const saveSafe = (name: string, address: string, threshold: number, ownersName: string[], ownersAddress: string[]) => (
dispatch: ReduxDispatch<GlobalState>,
) => {
const owners: List<Owner> = buildOwnersFrom(ownersName, ownersAddress)
const safe: Safe = SafeRecord({

View File

@ -13,7 +13,6 @@ export const calculateBalanceOf = async (tokenAddress: string, safeAddress: stri
if (tokenAddress === ETH_ADDRESS) {
return '0'
}
const erc20Token = await getStandardTokenContract()
let balance = 0
@ -33,15 +32,16 @@ const fetchTokenBalances = (safeAddress: string, tokens: List<Token>) => async (
if (!safeAddress || !tokens || !tokens.size) {
return
}
try {
const withBalances = await Promise.all(
tokens.map(async token => TokenBalanceRecord({
tokens.map(async (token) => {
const balance = await calculateBalanceOf(token.address, safeAddress, token.decimals)
return TokenBalanceRecord({
address: token.address,
balance: await calculateBalanceOf(token.address, safeAddress, token.decimals),
})),
balance,
})
}),
)
dispatch(updateSafe({ address: safeAddress, balances: List(withBalances) }))
} catch (err) {
// eslint-disable-next-line

View File

@ -9,15 +9,19 @@ const FrameDecorator = story => <div className={styles.frame}>{story()}</div>
storiesOf('Routes /welcome', module)
.addDecorator(FrameDecorator)
.add('Welcome with Gnosis Safe connected', () => {
const provider = select('Status by Provider', ['', 'UNKNOWN', 'SAFE', 'METAMASK', 'PARITY'], 'SAFE')
return <Component provider={provider} fetchProvider={() => {}} />
})
.add('Welcome with Metamask connected', () => {
const provider = select('Status by Provider', ['', 'UNKNOWN', 'METAMASK', 'PARITY'], 'METAMASK')
const provider = select('Status by Provider', ['', 'UNKNOWN', 'SAFE', 'METAMASK', 'PARITY'], 'METAMASK')
return <Component provider={provider} fetchProvider={() => {}} />
})
.add('Welcome with unknown wallet', () => {
const provider = select('Status by Provider', ['', 'UNKNOWN', 'METAMASK', 'PARITY'], 'UNKNOWN')
const provider = select('Status by Provider', ['', 'UNKNOWN', 'SAFE', 'METAMASK', 'PARITY'], 'UNKNOWN')
return <Component provider={provider} fetchProvider={() => {}} />
})
.add('Welcome without wallet connected', () => {
const provider = select('Status by Provider', ['', 'UNKNOWN', 'METAMASK', 'PARITY'], '')
const provider = select('Status by Provider', ['', 'UNKNOWN', 'SAFE', 'METAMASK', 'PARITY'], '')
return <Component provider={provider} fetchProvider={() => {}} />
})

View File

@ -30,7 +30,6 @@ afterAll(() => {
console.error = originalError
})
const renderLoadSafe = async (localStore: Store<GlobalState>) => {
const provider = await getProviderInfo()
const walletRecord = makeProvider(provider)