diff --git a/app/scripts/controllers/address-audit.js b/app/scripts/controllers/address-audit.js new file mode 100644 index 000000000..7b3fc132f --- /dev/null +++ b/app/scripts/controllers/address-audit.js @@ -0,0 +1,48 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') + +/** + * A controller that stores info about audited addresses + */ +class AddressAuditController { + /** + * Creates a AddressAuditController + * + * @param {Object} [config] - Options to configure controller + */ + constructor (opts = {}) { + const { initState } = opts + this.store = new ObservableStore(extend({ + addressAudits: {}, + }, initState)) + } + + add ({ address, auditor, status, message }) { + const currentState = this.store.getState() + const currentStateAudits = currentState.addressAudits + const currentAddressAudits = currentStateAudits && currentStateAudits[address] || {} + + this.store.updateState({ + addressAudits: { + ...currentStateAudits, + [address]: { + ...currentAddressAudits, + [auditor]: { + address, + auditor, + status, + message, + timestamp: Date.now(), + }, + }, + }, + }) + } + + clearAudits () { + this.store.updateState({ audits: {} }) + } + +} + +module.exports = AddressAuditController diff --git a/app/scripts/controllers/permissions/restrictedMethods.js b/app/scripts/controllers/permissions/restrictedMethods.js index 7ce9ce016..58f2fca35 100644 --- a/app/scripts/controllers/permissions/restrictedMethods.js +++ b/app/scripts/controllers/permissions/restrictedMethods.js @@ -13,6 +13,7 @@ const pluginRestrictedMethodDescriptions = { generateSignature: 'Sign messages with your account', // MetaMaskController#getApi + addAddressAudit: 'Check the recipients of your transaction and show you warnings if they are untrustworthy', addKnownMethodData: 'Update and store data about a known contract method', addNewAccount: 'Adds a new account to the default (first) HD seed phrase Keyring', addNewKeyring: 'Create a new keyring', diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 921e29932..4ada970ec 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -40,6 +40,7 @@ const DetectTokensController = require('./controllers/detect-tokens') const { PermissionsController } = require('./controllers/permissions') const PluginsController = require('./controllers/plugins') const AssetsController = require('./controllers/assets') +const AddressAuditController = require('./controllers/address-audit') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') @@ -263,6 +264,10 @@ module.exports = class MetamaskController extends EventEmitter { this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen }) + this.addressAuditController = new AddressAuditController({ + initState: initState.AddressAuditController, + }) + this.pluginsController = new PluginsController({ setupProvider: this.setupProvider.bind(this), _txController: this.txController, @@ -318,6 +323,7 @@ module.exports = class MetamaskController extends EventEmitter { PermissionsMetadata: this.permissionsController.store, PluginsController: this.pluginsController.store, ThreeBoxController: this.threeBoxController.store, + AddressAuditController: this.addressAuditController.store, }) this.memStore = new ComposableObservableStore(null, { @@ -343,6 +349,7 @@ module.exports = class MetamaskController extends EventEmitter { PermissionsMetadata: this.permissionsController.store, PluginsController: this.pluginsController.store, AssetsController: this.assetsController.store, + AddressAuditController: this.addressAuditController.store, ThreeBoxController: this.threeBoxController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) @@ -678,6 +685,9 @@ module.exports = class MetamaskController extends EventEmitter { // onboarding controller setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController), + + // addressAudit controller + addAddressAudit: this.addressAuditController.add.bind(this.addressAuditController), } } diff --git a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js index d26daf786..99ba74949 100644 --- a/ui/app/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/app/components/app/confirm-page-container/confirm-page-container.component.js @@ -25,6 +25,7 @@ export default class ConfirmPageContainer extends Component { toAddress: PropTypes.string, toName: PropTypes.string, toNickname: PropTypes.string, + recipientAudit: PropTypes.object, // Content contentComponent: PropTypes.node, errorKey: PropTypes.string, @@ -102,6 +103,7 @@ export default class ConfirmPageContainer extends Component { lastTx, ofText, requestsWaitingText, + recipientAudit, } = this.props const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress) @@ -130,6 +132,7 @@ export default class ConfirmPageContainer extends Component { recipientAddress={toAddress} recipientNickname={toNickname} assetImage={renderAssetImage ? assetImage : undefined} + audit={recipientAudit} /> { diff --git a/ui/app/components/ui/sender-to-recipient/index.scss b/ui/app/components/ui/sender-to-recipient/index.scss index 641015a81..8bd4ffac5 100644 --- a/ui/app/components/ui/sender-to-recipient/index.scss +++ b/ui/app/components/ui/sender-to-recipient/index.scss @@ -8,7 +8,7 @@ &--default { border-bottom: 1px solid $geyser; - height: 42px; + min-height: 42px; .sender-to-recipient { &__tooltip-wrapper { @@ -19,6 +19,25 @@ max-width: 100%; } + &__audit--warn, + &__audit--approve { + display: flex; + font-size: 12px; + width: 180px; + white-space: normal; + color: red; + } + + &__audit--approve { + color: green; + } + + &__party-group { + display: flex; + flex-direction: row; + align-items: center; + } + &__party { display: flex; flex-direction: row; @@ -44,6 +63,11 @@ } } + &__party--audit { + flex-direction: column; + align-items: flex-start; + } + &__arrow-container { position: absolute; height: 100%; diff --git a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js index c8e7a1870..186d22c71 100644 --- a/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js +++ b/ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js @@ -26,6 +26,7 @@ export default class SenderToRecipient extends PureComponent { assetImage: PropTypes.string, onRecipientClick: PropTypes.func, onSenderClick: PropTypes.func, + audit: PropTypes.func, } static defaultProps = { @@ -90,12 +91,14 @@ export default class SenderToRecipient extends PureComponent { renderRecipientWithAddress () { const { t } = this.context - const { recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props + const { recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick, audit = {} } = this.props const checksummedRecipientAddress = checksumAddress(recipientAddress) return (
{ this.setState({ recipientAddressCopied: true }) copyToClipboard(checksummedRecipientAddress) @@ -104,23 +107,35 @@ export default class SenderToRecipient extends PureComponent { } }} > - { this.renderRecipientIdenticon() } - this.setState({ recipientAddressCopied: false })} - > -
- { addressOnly ? `${t('to')}: ` : '' } - { - addressOnly - ? checksummedRecipientAddress - : (recipientNickname || recipientName || this.context.t('newContract')) - } -
-
+
+ { this.renderRecipientIdenticon() } + this.setState({ recipientAddressCopied: false })} + > +
+ { addressOnly ? `${t('to')}: ` : '' } + { + addressOnly + ? checksummedRecipientAddress + : (recipientNickname || recipientName || this.context.t('newContract')) + } +
+
+
+ { + Object.keys(audit).length + ?
+ {`${audit.auditor} ${audit.status === 'warning' ? 'warning' : 'approval'}: ${audit.message}`} +
+ : null + }
) } diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js index 623079e68..1fb33d472 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -96,6 +96,7 @@ export default class ConfirmTransactionBase extends Component { insufficientBalance: PropTypes.bool, hideFiatConversion: PropTypes.bool, transactionCategory: PropTypes.string, + recipientAudit: PropTypes.object, } state = { @@ -547,6 +548,7 @@ export default class ConfirmTransactionBase extends Component { warning, unapprovedTxCount, transactionCategory, + recipientAudit, } = this.props const { submitting, submitError } = this.state @@ -594,6 +596,7 @@ export default class ConfirmTransactionBase extends Component { onCancelAll={() => this.handleCancelAll()} onCancel={() => this.handleCancel()} onSubmit={() => this.handleSubmit()} + recipientAudit={recipientAudit} /> ) } diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js index 700883f17..a79476425 100644 --- a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -35,6 +35,7 @@ const mapStateToProps = (state, ownProps) => { const isMainnet = getIsMainnet(state) const { confirmTransaction, metamask } = state const { + addressAudits, conversionRate, identities, addressBook, @@ -76,6 +77,9 @@ const mapStateToProps = (state, ownProps) => { : addressSlicer(checksumAddress(toAddress)) ) + const recipientAudits = addressAudits[txParamsToAddress] || {} + const mostRecentAudit = Object.values(recipientAudits).sort((a, b) => a.timestamp > b.timestamp).find(audit => audit) + const addressBookObject = addressBook[checksumAddress(toAddress)] const toNickname = addressBookObject ? addressBookObject.name : '' const isTxReprice = Boolean(lastGasPrice) @@ -151,6 +155,7 @@ const mapStateToProps = (state, ownProps) => { hideFiatConversion: (!isMainnet && !showFiatInTestnets), metaMetricsSendCount, transactionCategory, + recipientAudit: mostRecentAudit, } }