diff --git a/packages/fether-react/package.json b/packages/fether-react/package.json index 99370cc896a7610c0ad3879b46c6868db2172c67..6b68b0cbe4894eba24b0b10053ed4fbce9eb3a15 100644 --- a/packages/fether-react/package.json +++ b/packages/fether-react/package.json @@ -38,8 +38,10 @@ "@parity/light.js": "https://github.com/parity-js/light.js#9646ce15d9dd9c4cf11776ddd613d5bd86016f94", "@parity/shared": "^3.0.2", "bignumber.js": "^4.1.0", + "debounce-promise": "^3.1.0", "debug": "^3.1.0", "fether-ui": "^0.1.0", + "final-form": "^4.8.3", "is-electron": "^2.1.0", "light-hoc": "^0.1.0", "lodash": "^4.17.10", @@ -48,10 +50,12 @@ "react": "^16.3.2", "react-blockies": "^1.3.0", "react-dom": "^16.3.2", + "react-final-form": "^3.6.4", "react-markdown": "^3.3.4", "react-router-dom": "^4.2.2", "react-scripts": "1.1.4", "react-tooltip": "^3.6.1", + "recompose": "^0.27.1", "rxjs": "^6.2.0" }, "devDependencies": { diff --git a/packages/fether-react/src/Send/Send.js b/packages/fether-react/src/Send/Send.js index f460e2724f0bd28f221bbef50c06739cb7408c07..231e073f77ac819f6ace059080f961fff0d8630d 100644 --- a/packages/fether-react/src/Send/Send.js +++ b/packages/fether-react/src/Send/Send.js @@ -4,32 +4,20 @@ // SPDX-License-Identifier: BSD-3-Clause import React, { Component } from 'react'; -import { inject, observer } from 'mobx-react'; import { Route, Redirect, Switch } from 'react-router-dom'; import Sent from './Sent'; import Signer from './Signer'; import TxForm from './TxForm'; -@inject('sendStore') -@observer class Send extends Component { render () { - const { - sendStore: { tokenAddress } - } = this.props; - - // We only show then Send components if we have already selected a token to - // send. - if (!tokenAddress) { - return ; - } - return ( - - - + + + + ); } diff --git a/packages/fether-react/src/Send/Signer/Signer.js b/packages/fether-react/src/Send/Signer/Signer.js index c91324f215b41abed11d39b66fd1f35b99d9da29..b491aee60d4c8541aab1463c463a8a069ae1fcb7 100644 --- a/packages/fether-react/src/Send/Signer/Signer.js +++ b/packages/fether-react/src/Send/Signer/Signer.js @@ -9,10 +9,14 @@ import { FormField, Header } from 'fether-ui'; import { inject, observer } from 'mobx-react'; import { Link } from 'react-router-dom'; import ReactTooltip from 'react-tooltip'; +import { withProps } from 'recompose'; import TokenBalance from '../../Tokens/TokensList/TokenBalance'; @inject('sendStore', 'tokensStore') +@withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({ + token: tokensStore.tokens[tokenAddress] +})) @observer class Signer extends Component { state = { @@ -58,11 +62,10 @@ class Signer extends Component { render () { const { - sendStore: { tokenAddress, tx }, - tokensStore + sendStore: { tx }, + token } = this.props; const { error, isSending, password } = this.state; - const token = tokensStore.tokens[tokenAddress]; return (
@@ -132,12 +135,6 @@ class Signer extends Component { />
- ); } diff --git a/packages/fether-react/src/Send/TxForm/TxForm.js b/packages/fether-react/src/Send/TxForm/TxForm.js index a93aca0bf46432feb3f07671f2bfd2eb743d9203..a5c93a14710b9bc6058350c9edb4e3190c3015dc 100644 --- a/packages/fether-react/src/Send/TxForm/TxForm.js +++ b/packages/fether-react/src/Send/TxForm/TxForm.js @@ -4,13 +4,15 @@ // SPDX-License-Identifier: BSD-3-Clause import React, { Component } from 'react'; -import debounce from 'lodash/debounce'; -import { FormField, Header } from 'fether-ui'; +import debounce from 'debounce-promise'; +import { estimateGas } from '../../utils/estimateGas'; +import { Field, Form } from 'react-final-form'; +import { Form as FetherForm, Header } from 'fether-ui'; import { fromWei, toWei } from '@parity/api/lib/util/wei'; import { inject, observer } from 'mobx-react'; import { isAddress } from '@parity/api/lib/util/address'; import { Link } from 'react-router-dom'; -import ReactTooltip from 'react-tooltip'; +import { withProps } from 'recompose'; import TokenBalance from '../../Tokens/TokensList/TokenBalance'; import withBalance from '../../utils/withBalance'; @@ -18,120 +20,25 @@ import withBalance from '../../utils/withBalance'; const MAX_GAS_PRICE = 40; // In Gwei const MIN_GAS_PRICE = 3; // Safelow gas price from GasStation, in Gwei -@inject('sendStore', 'tokensStore') -@withBalance( - ({ sendStore: { tokenAddress }, tokensStore }) => - tokensStore.tokens[tokenAddress] -) +@inject('parityStore', 'sendStore', 'tokensStore') +@withProps(({ match: { params: { tokenAddress } }, tokensStore }) => ({ + token: tokensStore.tokens[tokenAddress] +})) +@withBalance @observer class Send extends Component { - state = { - amount: '', // In Ether or in token - gasPrice: 4, // in Gwei - to: '', - estimating: false, // Currently estimating gasPrice - ...this.props.sendStore.tx - }; - - static getDerivedStateFromProps (nextProps, prevState) { - const { - balance, - sendStore: { estimated } - } = nextProps; - - // Calculate the maxAount - return { - maxAmount: - balance && estimated - ? +fromWei( - toWei(balance).minus( - estimated.mul(toWei(prevState.gasPrice, 'shannon')) - ) - ) - : 0.01 - }; - } - - componentDidMount () { - this.handleEstimateGasPrice(); - } - - estimateGas = debounce( - () => - this.props.sendStore - .estimateGas() - .then(() => this.setState({ estimating: false })) - .catch(() => this.setState({ estimating: false })), - 1000 - ); - - handleChangeAmount = ({ target: { value } }) => - this.setState({ amount: value }, this.handleEstimateGasPrice); - - handleChangeGasPrice = ({ target: { value } }) => - this.setState({ gasPrice: value }, this.handleEstimateGasPrice); - - handleChangeTo = ({ target: { value } }) => { - this.setState({ to: value }, this.handleEstimateGasPrice); - }; - - handleEstimateGasPrice = () => { - if (this.hasError()) { - return; - } - - const { amount, gasPrice, to } = this.state; - this.props.sendStore.setTx({ amount, gasPrice, to }); - this.setState({ estimating: true }, this.estimateGas); - }; - - handleMax = () => - this.setState( - { amount: this.state.maxAmount }, - this.handleEstimateGasPrice - ); - handleSubmit = event => { event.preventDefault(); - const { history } = this.props; - history.push('/send/signer'); - }; - - /** - * Get form errors. - * - * TODO Use a React form library to do this? - */ - hasError = () => { - const { amount, maxAmount, to } = this.state; - if (!amount || isNaN(amount)) { - return 'Please enter a valid amount'; - } - - if (amount < 0) { - return 'Please enter a positive amount '; - } - - if (amount > maxAmount) { - return "You don't have enough balance"; - } - - if (!isAddress(to)) { - return 'Please enter a valid Ethereum address'; - } - - return null; + const { history, sendStore, token } = this.props; + sendStore.setTx(event); + history.push(`/send/${token.address}/signer`); }; render () { const { - sendStore: { tokenAddress }, - tokensStore + sendStore: { tx }, + token } = this.props; - const { amount, estimating, gasPrice, maxAmount, to } = this.state; - - const token = tokensStore.tokens[tokenAddress]; - const error = this.hasError(); return (
@@ -141,7 +48,7 @@ class Send extends Component { Close } - title={

Send {token.name}

} + title={token &&

Send {token.name}

} />
@@ -149,104 +56,121 @@ class Send extends Component { -
- - - -
- } - label='Amount' - /> - - ( +
+
+ + + + + - } - label='To' - /> - - - - -
- } - label='Gas' - /> - - - + + + + )} + /> ]} - onClick={null} + onClick={null} // To disable cursor:pointer on card // TODO Can this be done better? token={token} /> - ); } + + /** + * Estimate gas amount, and validate that the user has enough balance to make + * the tx. + */ + validateBalance = debounce(async values => { + try { + const estimated = await estimateGas( + values, + this.props.token, + this.props.parityStore.api + ); + + const { balance } = this.props; + if (!balance || isNaN(estimated)) { + throw new Error('No "balance" or "estimated" value.'); + } + // Calculate the max amount the user can send + const maxAmount = +fromWei( + toWei(balance).minus(estimated.mul(toWei(values.gasPrice, 'shannon'))) + ); + + if (values.amount > maxAmount) { + return { amount: "You don't have enough balance" }; + } + } catch (err) { + return { + amount: 'Failed estimating balance, please try again' + }; + } + }, 1000); + + validateForm = values => { + if (!values.amount || isNaN(values.amount)) { + return { amount: 'Please enter a valid amount' }; + } + + if (values.amount < 0) { + return { amount: 'Please enter a positive amount ' }; + } + + if (this.props.balance && this.props.balance.lt(values.amount)) { + return { amount: "You don't have enough balance" }; + } + + if (!isAddress(values.to)) { + return { to: 'Please enter a valid Ethereum address' }; + } + + return this.validateBalance(values); + }; } export default Send; diff --git a/packages/fether-react/src/Tokens/TokensList/TokenBalance/TokenBalance.js b/packages/fether-react/src/Tokens/TokensList/TokenBalance/TokenBalance.js index faa0a2004b1a3b91d9374d9c7b94107a2aeb3714..08ea0a5fc9b1c30bf7a22b9ec55b1b9364fb9c68 100644 --- a/packages/fether-react/src/Tokens/TokensList/TokenBalance/TokenBalance.js +++ b/packages/fether-react/src/Tokens/TokensList/TokenBalance/TokenBalance.js @@ -11,12 +11,12 @@ import { withRouter } from 'react-router-dom'; import withBalance from '../../../utils/withBalance'; -@withBalance() +@withBalance @inject('sendStore') @withRouter class TokenBalance extends Component { static propTypes = { - token: PropTypes.object.isRequired + token: PropTypes.object }; handleClick = () => { @@ -25,8 +25,7 @@ class TokenBalance extends Component { return; } sendStore.clear(); - sendStore.setTokenAddress(token.address); - history.push('/send'); + history.push(`/send/${token.address}`); }; render () { diff --git a/packages/fether-react/src/stores/sendStore.js b/packages/fether-react/src/stores/sendStore.js index 2bfc036e3a153d288b2936dff4f926d7b8a6e83b..8103f6750839a1752388df2e3e9f3b9395aef70c 100644 --- a/packages/fether-react/src/stores/sendStore.js +++ b/packages/fether-react/src/stores/sendStore.js @@ -9,11 +9,11 @@ import { BigNumber } from 'bignumber.js'; import { blockNumber$, makeContract$, post$ } from '@parity/light.js'; import memoize from 'lodash/memoize'; import noop from 'lodash/noop'; -import { toWei } from '@parity/api/lib/util/wei'; import Debug from '../utils/debug'; import parityStore from './parityStore'; import tokensStore from './tokensStore'; +import { txForErc20, txForEth } from '../utils/estimateGas'; const debug = Debug('sendStore'); const GAS_MULT_FACTOR = 1.25; // Since estimateGas is not always accurate, we add a 33% factor for buffer. @@ -75,9 +75,11 @@ export class SendStore { } if (this.tokenAddress === 'ETH') { - return this.estimateGasForEth(this.txForEth); + return this.estimateGasForEth(txForEth(this.tx)); } else { - return this.estimateGasForErc20(this.txForErc20); + return this.estimateGasForErc20( + txForErc20(this.tx, tokensStore.tokens[this.tokenAddress]) + ); } }; @@ -113,10 +115,10 @@ export class SendStore { send = password => { const send$ = this.tokenAddress === 'ETH' - ? post$(this.txForEth) + ? post$(txForEth(this.tx)) : this.contract.transfer$( - ...this.txForErc20.args, - this.txForErc20.options + ...txForErc20(this.tx).args, + txForErc20(this.tx).options ); debug( @@ -138,38 +140,6 @@ export class SendStore { }); }; - /** - * This.tx is a user-friendly tx object. We convert it now as it can be - * passed to makeContract$(...). - */ - @computed - get txForErc20 () { - return { - args: [ - this.tx.to, - new BigNumber(this.tx.amount).mul( - new BigNumber(10).pow(tokensStore.tokens[this.tokenAddress].decimals) - ) - ], - options: { - gasPrice: toWei(this.tx.gasPrice, 'shannon') // shannon == gwei - } - }; - } - - /** - * This.tx is a user-friendly tx object. We convert it now as it can be - * passed to post$(tx). - */ - @computed - get txForEth () { - return { - gasPrice: toWei(this.tx.gasPrice, 'shannon'), // shannon == gwei - to: this.tx.to, - value: toWei(this.tx.amount.toString()) - }; - } - @action setBlockNumber = blockNumber => { this.blockNumber = blockNumber; diff --git a/packages/fether-react/src/utils/estimateGas.js b/packages/fether-react/src/utils/estimateGas.js new file mode 100644 index 0000000000000000000000000000000000000000..375f119ba55c705adde34dcbc83757f8b1b8ed90 --- /dev/null +++ b/packages/fether-react/src/utils/estimateGas.js @@ -0,0 +1,90 @@ +// Copyright 2015-2018 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import BigNumber from 'bignumber.js'; +import memoize from 'lodash/memoize'; +import { toWei } from '@parity/api/lib/util/wei'; + +import Debug from './debug'; + +const debug = Debug('estimateGas'); +const GAS_MULT_FACTOR = 1.25; // Since estimateGas is not always accurate, we add a 33% factor for buffer. + +/** + * Estimate the amount of gas for our transaction. + */ +export const estimateGas = (tx, token, api) => { + if (!tx || !Object.keys(tx).length) { + return Promise.reject(new Error('Tx not set in sendStore.')); + } + + if (token.address === 'ETH') { + return estimateGasForEth(txForEth(tx), api).then(addBuffer); + } else { + return estimateGasForErc20(txForErc20(tx, token), api).then(addBuffer); + } +}; + +/** + * Estimate gas to transfer in ERC20 contract. Expensive function, so we + * memoize it. + */ +const estimateGasForErc20 = memoize( + txForErc20 => + this.contract.contractObject.instance.transfer.estimateGas( + txForErc20.options, + txForErc20.args + ), + JSON.stringify +); + +/** + * Estimate gas to transfer to an ETH address. Expensive function, so we + * memoize it. + */ +const estimateGasForEth = memoize((txForEth, api) => { + debug(`Estimating gas for tx.`, txForEth); + return api.eth.estimateGas(txForEth); +}, JSON.stringify); + +/** + * Add some extra gas buffer just to be sure user has enough balance. + * + * @param {BigNumber} estimated - The estimated gas price returned by + * estimateGas. + */ +const addBuffer = estimated => { + const withBuffer = estimated.mul(GAS_MULT_FACTOR); + debug(`Estimated gas ${+estimated}, with buffer ${+withBuffer}.`); + return withBuffer; +}; + +/** + * This.tx is a user-friendly tx object. We convert it now as it can be + * passed to makeContract$.transfer(...). + */ +export const txForErc20 = (tx, token) => { + return { + args: [ + tx.to, + new BigNumber(tx.amount).mul(new BigNumber(10).pow(token.decimals)) + ], + options: { + gasPrice: toWei(tx.gasPrice, 'shannon') // shannon == gwei + } + }; +}; + +/** + * This.tx is a user-friendly tx object. We convert it now as it can be + * passed to post$(tx). + */ +export const txForEth = tx => { + return { + gasPrice: toWei(tx.gasPrice, 'shannon'), // shannon == gwei + to: tx.to, + value: toWei(tx.amount.toString()) + }; +}; diff --git a/packages/fether-react/src/utils/withBalance.js b/packages/fether-react/src/utils/withBalance.js index e852cadbda20526242773bf4f3aad7cf6abf92f9..c4442e731e3a63ba0c27d7ea9a2d3041117a5ec5 100644 --- a/packages/fether-react/src/utils/withBalance.js +++ b/packages/fether-react/src/utils/withBalance.js @@ -16,35 +16,35 @@ import { fromWei } from '@parity/api/lib/util/wei'; import light from 'light-hoc'; /** - * A HOC on light.js to get the current balance. + * A HOC on light.js to get the current balance. The inner component needs to + * have a `token` field in its props. * * @example - * @withBalance() + * @withBalance * class MyComponent extends React.Component{ * * } */ -export default (propsSelector = ({ token }) => token) => - light({ - balance: ownProps => { - // Find our token object in the props - const token = propsSelector(ownProps); +export default light({ + balance: ownProps => { + // Find our token object in the props + const { token } = ownProps; - if (!token.address) { - return empty(); - } - return token.address === 'ETH' - ? myBalance$().pipe( - map(value => (isNullOrLoading(value) ? null : value)), // Transform loading state to null - map(value => value && fromWei(value)) - ) - : defaultAccount$().pipe( - filter(x => x), - switchMap(defaultAccount => - makeContract$(token.address, abi).balanceOf$(defaultAccount) - ), - map(value => (isNullOrLoading(value) ? null : value)), // Transform loading state to null - map(value => value && value.div(10 ** token.decimals)) - ); + if (!token || !token.address) { + return empty(); } - }); + return token.address === 'ETH' + ? myBalance$().pipe( + map(value => (isNullOrLoading(value) ? null : value)), // Transform loading state to null + map(value => value && fromWei(value)) + ) + : defaultAccount$().pipe( + filter(x => x), + switchMap(defaultAccount => + makeContract$(token.address, abi).balanceOf$(defaultAccount) + ), + map(value => (isNullOrLoading(value) ? null : value)), // Transform loading state to null + map(value => value && value.div(10 ** token.decimals)) + ); + } +}); diff --git a/yarn.lock b/yarn.lock index 90f7dd459b7db5c6477e6a27b126b7732d335527..b8d3e188387b8b96f4d0ef3d5e41f58a68e3e961 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2919,6 +2919,10 @@ chalk@~0.4.0: has-color "~0.1.0" strip-ansi "~0.1.0" +change-emitter@^0.1.2: + version "0.1.6" + resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" + character-entities-legacy@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz#7c6defb81648498222c9855309953d05f4d63a9c" @@ -3870,6 +3874,10 @@ dateformat@^3.0.0, dateformat@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" +debounce-promise@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debounce-promise/-/debounce-promise-3.1.0.tgz#25035f4b45017bd51a7bef8b3bd9f6401dc47423" + debug-log@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" @@ -5190,7 +5198,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.16: +fbjs@^0.8.1, fbjs@^0.8.16: version "0.8.17" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" dependencies: @@ -5276,6 +5284,10 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +final-form@^4.8.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/final-form/-/final-form-4.8.3.tgz#86a03da6cd6459ed8fe3737dbd9dc87ed40c11d7" + finalhandler@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" @@ -5993,7 +6005,7 @@ hoist-non-react-statics@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" -hoist-non-react-statics@^2.5.0: +hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" @@ -7053,6 +7065,10 @@ jest@20.0.4: dependencies: jest-cli "^20.0.4" +jquery@x.*: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" + js-base64@^2.1.8, js-base64@^2.1.9: version "2.4.5" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.5.tgz#e293cd3c7c82f070d700fc7a1ca0a2e69f101f92" @@ -7284,6 +7300,10 @@ keccak@^1.0.2: nan "^2.2.1" safe-buffer "^5.1.0" +keyboard-key@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/keyboard-key/-/keyboard-key-1.0.1.tgz#a946294fe59ad5431c63a3ea269f023e51fac6aa" + keyv@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" @@ -9552,6 +9572,10 @@ react-error-overlay@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" +react-final-form@^3.6.4: + version "3.6.4" + resolved "https://registry.yarnpkg.com/react-final-form/-/react-final-form-3.6.4.tgz#1ca37935c2af0bc659a53b293dd84a75d2381548" + react-lifecycles-compat@^3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -9795,6 +9819,17 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +recompose@^0.27.1: + version "0.27.1" + resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba" + dependencies: + babel-runtime "^6.26.0" + change-emitter "^0.1.2" + fbjs "^0.8.1" + hoist-non-react-statics "^2.3.1" + react-lifecycles-compat "^3.0.2" + symbol-observable "^1.0.4" + recursive-readdir@2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.1.tgz#90ef231d0778c5ce093c9a48d74e5c5422d13a99" @@ -10353,6 +10388,23 @@ selfsigned@^1.9.1: dependencies: node-forge "0.7.5" +semantic-ui-css@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/semantic-ui-css/-/semantic-ui-css-2.3.2.tgz#f84ffaf7559c17401db43e2b44b7b4caaab54836" + dependencies: + jquery x.* + +semantic-ui-react@^0.81.3: + version "0.81.3" + resolved "https://registry.yarnpkg.com/semantic-ui-react/-/semantic-ui-react-0.81.3.tgz#ad98917c44cda4b316ee841d67dc071e16e93e9c" + dependencies: + "@babel/runtime" "^7.0.0-beta.49" + classnames "^2.2.5" + keyboard-key "^1.0.1" + lodash "^4.17.10" + prop-types "^15.6.1" + shallowequal "^1.0.2" + semistandard@^12.0.1: version "12.0.1" resolved "https://registry.yarnpkg.com/semistandard/-/semistandard-12.0.1.tgz#82190c720b01cf68e3051e0985578f0b1d596683" @@ -10474,6 +10526,10 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shallowequal@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -11082,7 +11138,7 @@ symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" -symbol-observable@^1.0.2, symbol-observable@^1.1.0: +symbol-observable@^1.0.2, symbol-observable@^1.0.4, symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"