Browse Source

New contracts app (#1283)

* new contracts app

* new contracts app

* test ci fix

* fixes

* fixes

* remove abi inline

* css code card

* icon buttons

* fixes

* fixes cont.

* travis ci fixed

* api version
kwingram25 5 years ago
parent
commit
4b7987ae1c
73 changed files with 3108 additions and 1489 deletions
  1. 2 2
      package.json
  2. 4 4
      packages/app-accounts/src/Account.tsx
  3. 1 0
      packages/app-accounts/src/index.tsx
  4. 0 74
      packages/app-accounts/src/modals/Forgetting.tsx
  5. 1 1
      packages/app-accounts/src/modals/Transfer.tsx
  6. 7 6
      packages/app-address-book/src/Address.tsx
  7. 1 0
      packages/app-address-book/src/index.tsx
  8. 0 83
      packages/app-address-book/src/modals/Forgetting.tsx
  9. 132 22
      packages/app-contracts/src/ABI.tsx
  10. 0 285
      packages/app-contracts/src/Code.tsx
  11. 117 0
      packages/app-contracts/src/Codes/Add.tsx
  12. 172 0
      packages/app-contracts/src/Codes/Code.tsx
  13. 118 0
      packages/app-contracts/src/Codes/Upload.tsx
  14. 1 1
      packages/app-contracts/src/Codes/ValidateCode.tsx
  15. 98 0
      packages/app-contracts/src/Codes/index.tsx
  16. 133 0
      packages/app-contracts/src/Contracts/Add.tsx
  17. 170 50
      packages/app-contracts/src/Contracts/Call.tsx
  18. 160 0
      packages/app-contracts/src/Contracts/Contract.tsx
  19. 9 6
      packages/app-contracts/src/Contracts/ValidateAddr.tsx
  20. 124 0
      packages/app-contracts/src/Contracts/index.tsx
  21. 290 0
      packages/app-contracts/src/Deploy.tsx
  22. 0 371
      packages/app-contracts/src/Instantiate.tsx
  23. 240 0
      packages/app-contracts/src/Modal.tsx
  24. 3 1
      packages/app-contracts/src/Params.tsx
  25. 94 0
      packages/app-contracts/src/RemoveABI.tsx
  26. 79 54
      packages/app-contracts/src/index.tsx
  27. 19 47
      packages/app-contracts/src/store.ts
  28. 17 8
      packages/app-contracts/src/types.ts
  29. 1 0
      packages/app-council/src/Overview/Candidate.tsx
  30. 1 0
      packages/app-council/src/Overview/Member.tsx
  31. 1 0
      packages/app-democracy/src/index.tsx
  32. 1 0
      packages/app-explorer/src/index.tsx
  33. 1 0
      packages/app-extrinsics/src/index.tsx
  34. 1 0
      packages/app-settings/src/index.tsx
  35. 1 0
      packages/app-staking/src/Account/index.tsx
  36. 2 2
      packages/app-staking/src/Accounts.tsx
  37. 1 0
      packages/app-staking/src/Overview/Address.tsx
  38. 1 0
      packages/app-staking/src/index.tsx
  39. 1 0
      packages/app-storage/src/Selection/index.tsx
  40. 1 0
      packages/app-sudo/src/index.tsx
  41. 1 0
      packages/app-toolbox/src/index.tsx
  42. 1 1
      packages/ui-api/package.json
  43. 1 0
      packages/ui-api/src/Api.tsx
  44. 6 3
      packages/ui-app/src/AddressCard.tsx
  45. 5 3
      packages/ui-app/src/AddressMini.tsx
  46. 34 317
      packages/ui-app/src/AddressRow.tsx
  47. 15 2
      packages/ui-app/src/Button/Button.tsx
  48. 2 0
      packages/ui-app/src/Button/types.ts
  49. 6 0
      packages/ui-app/src/Card.tsx
  50. 59 16
      packages/ui-app/src/CardGrid.tsx
  51. 200 0
      packages/ui-app/src/CodeRow.tsx
  52. 141 0
      packages/ui-app/src/Forget.tsx
  53. 2 2
      packages/ui-app/src/InputAddress.tsx
  54. 40 26
      packages/ui-app/src/InputFile.tsx
  55. 117 0
      packages/ui-app/src/Messages.tsx
  56. 1 1
      packages/ui-app/src/Params/Extrinsic.tsx
  57. 312 0
      packages/ui-app/src/Row.tsx
  58. 37 15
      packages/ui-app/src/Tabs.tsx
  59. 3 0
      packages/ui-app/src/index.tsx
  60. 1 1
      packages/ui-app/src/styles/app.css
  61. 1 0
      packages/ui-app/src/styles/semantic.css
  62. 5 6
      packages/ui-app/src/util/getAddressName.ts
  63. 3 4
      packages/ui-app/src/util/getAddressTags.ts
  64. 25 0
      packages/ui-app/src/util/getContractAbi.ts
  65. 3 2
      packages/ui-app/src/util/index.ts
  66. 1 0
      packages/ui-params/src/Param/Account.tsx
  67. 1 1
      packages/ui-params/src/Param/Base.tsx
  68. 1 1
      packages/ui-params/src/Param/File.tsx
  69. 10 3
      packages/ui-params/src/Param/index.tsx
  70. 1 1
      packages/ui-params/src/types.ts
  71. 1 2
      packages/ui-signer/src/Modal.tsx
  72. 2 0
      tsconfig.json
  73. 65 65
      yarn.lock

+ 2 - 2
package.json

@@ -10,9 +10,9 @@
     "packages/*"
   ],
   "resolutions": {
-    "@polkadot/api": "^0.81.0-beta.3",
+    "@polkadot/api": "^0.81.0-beta.7",
     "@polkadot/keyring": "^0.93.0-beta.0",
-    "@polkadot/types": "^0.81.0-beta.3",
+    "@polkadot/types": "^0.81.0-beta.7",
     "@polkadot/util": "^0.93.0-beta.0",
     "@polkadot/util-crypto": "^0.93.0-beta.0",
     "babel-core": "^7.0.0-bridge.0",

+ 4 - 4
packages/app-accounts/src/Account.tsx

@@ -6,12 +6,11 @@ import { ActionStatus } from '@polkadot/ui-app/Status/types';
 import { I18nProps } from '@polkadot/ui-app/types';
 
 import React from 'react';
-import { AddressCard, AddressInfo, Button, Icon } from '@polkadot/ui-app';
+import { AddressCard, AddressInfo, Button, Forget, Icon } from '@polkadot/ui-app';
 import keyring from '@polkadot/ui-keyring';
 
 import Backup from './modals/Backup';
 import ChangePass from './modals/ChangePass';
-import Forgetting from './modals/Forgetting';
 import Transfer from './modals/Transfer';
 
 import translate from './translate';
@@ -53,6 +52,7 @@ class Account extends React.PureComponent<Props> {
       <AddressCard
         buttons={this.renderButtons()}
         isEditable={isEditable}
+        type='account'
         value={address}
         withExplorer
         withIndex
@@ -90,9 +90,9 @@ class Account extends React.PureComponent<Props> {
 
     if (isForgetOpen) {
       modals.push(
-        <Forgetting
+        <Forget
           address={address}
-          doForget={this.onForget}
+          onForget={this.onForget}
           key='modal-forget-account'
           onClose={this.toggleForget}
         />

+ 1 - 0
packages/app-accounts/src/index.tsx

@@ -42,6 +42,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
       ...baseState,
       tabs: [
         {
+          isRoot: true,
           name: 'overview',
           text: t('My accounts')
         },

+ 0 - 74
packages/app-accounts/src/modals/Forgetting.tsx

@@ -1,74 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-accounts authors & contributors
-// This software may be modified and distributed under the terms
-// of the Apache-2.0 license. See the LICENSE file for details.
-
-import { I18nProps } from '@polkadot/ui-app/types';
-
-import React from 'react';
-import { AddressRow, Button, Modal } from '@polkadot/ui-app';
-
-import translate from '../translate';
-
-type Props = I18nProps & {
-  address: string,
-  onClose: () => void,
-  doForget: () => void
-};
-
-class Forgetting extends React.PureComponent<Props> {
-  render () {
-    const { t } = this.props;
-
-    return (
-      <Modal
-        className='app--accounts-Modal'
-        dimmer='inverted'
-        open
-      >
-        <Modal.Header>{t('Confirm account removal')}</Modal.Header>
-        {this.renderContent()}
-        {this.renderButtons()}
-      </Modal>
-    );
-  }
-
-  private renderButtons () {
-    const { onClose, doForget, t } = this.props;
-
-    return (
-      <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            onClick={onClose}
-            label={t('Cancel')}
-          />
-          <Button.Or />
-          <Button
-            isPrimary
-            onClick={doForget}
-            label={t('Forget')}
-          />
-        </Button.Group>
-      </Modal.Actions>
-    );
-  }
-
-  private renderContent () {
-    const { address, t } = this.props;
-
-    return (
-      <Modal.Content>
-        <AddressRow
-          isInline
-          value={address}
-        >
-          <p>{t('You are about to remove this account from your list of available accounts. Once completed, should you need to access it again, you will have to re-create the account either via seed or via a backup file.')}</p>
-          <p>{t('This operaion does not remove the history of the account from the chain, nor any associated funds from the account. The forget operation only limits your access to the account on this browser.')}</p>
-        </AddressRow>
-      </Modal.Content>
-    );
-  }
-}
-
-export default translate(Forgetting);

+ 1 - 1
packages/app-accounts/src/modals/Transfer.tsx

@@ -180,7 +180,7 @@ class Transfer extends React.PureComponent<Props> {
             isDisabled={!!propRecipientId}
             label={t('send to address')}
             onChange={this.onChangeTo}
-            type='all'
+            type='allPlus'
           />
           <div className='balance'><Available label={available} params={recipientId} /></div>
           <InputBalance

+ 7 - 6
packages/app-address-book/src/Address.tsx

@@ -7,11 +7,10 @@ import { ActionStatus } from '@polkadot/ui-app/Status/types';
 import { I18nProps } from '@polkadot/ui-app/types';
 
 import React from 'react';
-import { AddressCard, AddressInfo, Button, Icon } from '@polkadot/ui-app';
+import { AddressCard, AddressInfo, Button, Forget, Icon } from '@polkadot/ui-app';
 import keyring from '@polkadot/ui-keyring';
 
 import Transfer from '@polkadot/app-accounts/modals/Transfer';
-import Forgetting from './modals/Forgetting';
 
 import translate from './translate';
 
@@ -50,6 +49,7 @@ class Address extends React.PureComponent<Props, State> {
       <AddressCard
         buttons={this.renderButtons()}
         isEditable={isEditable}
+        type='address'
         value={address}
         withExplorer
         withIndex
@@ -77,10 +77,11 @@ class Address extends React.PureComponent<Props, State> {
 
     if (isForgetOpen) {
       modals.push(
-        <Forgetting
-          currentAddress={current}
-          doForget={this.onForget}
-          key='modal-forget'
+        <Forget
+          address={current.address()}
+          onForget={this.onForget}
+          key='modal-forget-account'
+          mode='address'
           onClose={this.toggleForget}
         />
       );

+ 1 - 0
packages/app-address-book/src/index.tsx

@@ -43,6 +43,7 @@ class AddressBookApp extends React.PureComponent<Props, State> {
       isCreateOpen: false,
       items: [
         {
+          isRoot: true,
           name: 'overview',
           text: t('My contacts')
         }

+ 0 - 83
packages/app-address-book/src/modals/Forgetting.tsx

@@ -1,83 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-accounts authors & contributors
-// This software may be modified and distributed under the terms
-// of the Apache-2.0 license. See the LICENSE file for details.
-
-import { KeyringAddress } from '@polkadot/ui-keyring/types';
-import { I18nProps } from '@polkadot/ui-app/types';
-
-import React from 'react';
-import { AddressRow, Button, Modal } from '@polkadot/ui-app';
-
-import translate from '../translate';
-
-type Props = I18nProps & {
-  onClose: () => void,
-  doForget: () => void,
-  currentAddress: KeyringAddress | null
-};
-
-class Forgetting extends React.PureComponent<Props> {
-  constructor (props: Props) {
-    super(props);
-  }
-
-  render () {
-    const { style, t } = this.props;
-
-    return (
-      <Modal
-        dimmer='inverted'
-        open
-        style={style}
-      >
-        <Modal.Header>{t('Confirm address removal')}</Modal.Header>
-        {this.renderContent()}
-        {this.renderButtons()}
-      </Modal>
-    );
-  }
-
-  private renderButtons () {
-    const { onClose, doForget, t } = this.props;
-
-    return (
-      <Modal.Actions>
-        <Button.Group>
-          <Button
-            isNegative
-            onClick={onClose}
-            label={t('Cancel')}
-          />
-          <Button.Or />
-          <Button
-            isPrimary
-            onClick={doForget}
-            label={t('Forget')}
-          />
-        </Button.Group>
-      </Modal.Actions>
-    );
-  }
-
-  private renderContent () {
-    const { t, currentAddress } = this.props;
-
-    const address = currentAddress
-      ? currentAddress.address()
-      : undefined;
-
-    return (
-      <Modal.Content>
-        <AddressRow
-          isInline
-          value={address || ''}
-        >
-          <p>{t('You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.')}</p>
-          <p>{t('This operation does not remove the history of the account from the chain, nor any associated funds from the account. The forget operation only limits your access to the address on this browser.')}</p>
-        </AddressRow>
-      </Modal.Content>
-    );
-  }
-}
-
-export default translate(Forgetting);

+ 132 - 22
packages/app-contracts/src/ABI.tsx

@@ -5,65 +5,175 @@
 import { I18nProps } from '@polkadot/ui-app/types';
 
 import React from 'react';
-import { InputFile } from '@polkadot/ui-app';
+import styled from 'styled-components';
+import { InputFile, Labelled, Messages } from '@polkadot/ui-app';
 import { ContractAbi } from '@polkadot/types';
 import { u8aToString } from '@polkadot/util';
 
 import translate from './translate';
 
 type Props = I18nProps & {
-  help: React.ReactNode,
+  contractAbi?: ContractAbi | null,
+  help?: React.ReactNode,
   isError?: boolean,
-  label: React.ReactNode,
-  onChange: (json: string | null, contractAbi: ContractAbi | null) => void
+  isDisabled?: boolean,
+  isRequired?: boolean,
+  label?: React.ReactNode,
+  onChange: (json: string | null, contractAbi: ContractAbi | null) => void,
+  onRemove?: () => void,
+  onRemoved?: () => void,
+  onSelect?: () => void
 };
 
 type State = {
-  abi?: Uint8Array | null,
+  contractAbi: ContractAbi | null,
   isAbiValid: boolean,
-  name?: string,
-  placeholder?: React.ReactNode | null
+  isEmpty: boolean,
+  isError: boolean
 };
 
+const Normalize = styled.div`
+  min-height: 4rem;
+`;
+
 class ABI extends React.PureComponent<Props, State> {
   state: State = {
-    isAbiValid: true
+    contractAbi: null,
+    isAbiValid: false,
+    isEmpty: true,
+    isError: false
   };
 
+  constructor (props: Props) {
+    super(props);
+
+    const { contractAbi, isError, isRequired } = this.props;
+
+    const isAbiValid = !!contractAbi;
+
+    this.state = {
+      contractAbi: contractAbi || null,
+      isAbiValid,
+      isEmpty: !isAbiValid,
+      isError: isError || (isRequired && !isAbiValid) || false
+    };
+  }
+
+  componentWillReceiveProps ({ contractAbi, isError, isRequired }: Props) {
+    if (contractAbi) {
+      this.setState({
+        contractAbi,
+        isAbiValid: true,
+        isError: false
+      });
+    } else if (this.props.contractAbi) {
+      this.setState({
+        contractAbi: null,
+        isAbiValid: false,
+        isError: isError || isRequired || false
+      });
+    }
+  }
+
   render () {
-    const { help, isError, label } = this.props;
-    const { isAbiValid, placeholder } = this.state;
+    const { contractAbi, isAbiValid } = this.state;
+
+    return (contractAbi && isAbiValid)
+      ? this.renderMessages()
+      : this.renderInputFile();
+  }
+
+  private renderInputFile () {
+    const { help, isDisabled, isRequired, label, t } = this.props;
+    const { isAbiValid, isEmpty, isError } = this.state;
 
     return (
-      <InputFile
-        help={help}
-        isError={!isAbiValid || isError}
-        label={label}
-        onChange={this.onChange}
-        placeholder={placeholder}
+      <Normalize>
+        <InputFile
+          help={help}
+          isDisabled={isDisabled}
+          isError={!isAbiValid && (isRequired || isError)}
+          label={label}
+          onChange={this.onChange}
+          placeholder={
+            !isEmpty && !isAbiValid
+              ? t('invalid ABI file selected')
+              : t('click to select or drag and drop a JSON ABI file')
+          }
+        />
+      </Normalize>
+    );
+  }
+
+  private renderMessages () {
+    const { help, isDisabled, label, onRemove } = this.props;
+    const { contractAbi } = this.state;
+
+    const messages = (
+      <Messages
+        contractAbi={contractAbi!}
+        onRemove={onRemove || this.onRemove}
+        isRemovable={!isDisabled}
       />
     );
+
+    if (label) {
+      return (
+        <Normalize>
+          <Labelled
+            label={label}
+            help={help}
+          >
+              {messages}
+          </Labelled>
+        </Normalize>
+      );
+    }
+    return (
+      <Normalize>
+        {messages}
+      </Normalize>
+    );
   }
 
-  private onChange = (u8a: Uint8Array, name: string): void => {
+  private onChange = (u8a: Uint8Array): void => {
     const { onChange } = this.props;
     const json = u8aToString(u8a);
 
     try {
-      const abi = new ContractAbi(JSON.parse(json));
+      const contractAbi = new ContractAbi(JSON.parse(json));
 
       this.setState({
+        contractAbi,
         isAbiValid: true,
-        name,
-        placeholder: `${name} (${Object.keys(abi.messages).join(', ')})`
-      }, () => onChange(json, abi));
+        isEmpty: false,
+        isError: false
+      }, () => onChange(json, contractAbi));
     } catch (error) {
+      console.error(error);
       this.setState({
         isAbiValid: false,
-        placeholder: error.message
+        isEmpty: false,
+        isError: true
       }, () => onChange(null, null));
     }
   }
+
+  private onRemove = (): void => {
+    const { onChange, onRemoved } = this.props;
+
+    this.setState(
+      {
+        contractAbi: null,
+        isAbiValid: false,
+        isEmpty: true
+      },
+      () => {
+        onChange(null, null);
+        onRemoved && onRemoved();
+      }
+    );
+  }
 }
 
 export default translate(ABI);

+ 0 - 285
packages/app-contracts/src/Code.tsx

@@ -1,285 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-contracts authors & contributors
-// This software may be modified and distributed under the terms
-// of the Apache-2.0 license. See the LICENSE file for details.
-
-import { SubmittableResult } from '@polkadot/api/SubmittableExtrinsic';
-import { I18nProps } from '@polkadot/ui-app/types';
-import { ComponentProps } from './types';
-
-import BN from 'bn.js';
-import React from 'react';
-import { Button, Input, InputAddress, InputFile, InputNumber, TxButton, TxComponent } from '@polkadot/ui-app';
-import { compactAddLength } from '@polkadot/util';
-import { Hash } from '@polkadot/types';
-
-import ABI from './ABI';
-import ValidateCode from './ValidateCode';
-import store from './store';
-import translate from './translate';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  abi?: string | null,
-  accountId?: string | null,
-  codeHash?: string | null,
-  gasLimit: BN,
-  isAbiValid: boolean,
-  isBusy: boolean,
-  isCodeValid: boolean,
-  isNameValid: boolean,
-  isNew: boolean,
-  isWasmValid: boolean,
-  name?: string | null,
-  wasm?: Uint8Array | null
-};
-
-class Deploy extends TxComponent<Props, State> {
-  state: State = {
-    accountId: null,
-    gasLimit: new BN(0),
-    isAbiValid: true,
-    isBusy: false,
-    isCodeValid: false,
-    isNew: true,
-    isNameValid: false,
-    isWasmValid: false
-  };
-
-  render () {
-    const { t } = this.props;
-    const { isNew } = this.state;
-
-    return (
-      <div className='contracts--Code'>
-        <Button.Group isBasic isCentered>
-          <Button
-            isBasic
-            isNegative={isNew}
-            label={t('deploy new')}
-            onClick={this.toggleNew}
-          />
-          <Button
-            isBasic
-            isNegative={!isNew}
-            label={t('attach existing')}
-            onClick={this.toggleNew}
-          />
-        </Button.Group>
-        {
-          isNew
-            ? this.renderDeploy()
-            : this.renderExisting()
-        }
-      </div>
-    );
-  }
-
-  private renderDeploy () {
-    const { t } = this.props;
-    const { accountId, gasLimit, isAbiValid, isBusy, isNameValid, isWasmValid, wasm } = this.state;
-    const isValid = !isBusy && isAbiValid && isNameValid && isWasmValid && !gasLimit.isZero() && !!accountId;
-
-    return (
-      <>
-        <InputAddress
-          help={t('Specify the user account to use for this deployment. And fees will be deducted from this account.')}
-          label={t('deployment account')}
-          onChange={this.onChangeAccount}
-          type='account'
-        />
-        <InputFile
-          help={t('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
-          isError={!isWasmValid}
-          label={t('compiled contract WASM')}
-          onChange={this.onAddWasm}
-          placeholder={
-            wasm && !isWasmValid
-              ? t('The code is not recognized as being in valid WASM format')
-              : null
-          }
-        />
-        {this.renderInputName()}
-        {this.renderInputAbi()}
-        <InputNumber
-          help={t('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
-          label={t('maximum gas allowed')}
-          onChange={this.onChangeGas}
-          onEnter={this.sendTx}
-        />
-        <Button.Group>
-          <TxButton
-            accountId={accountId}
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Deploy')}
-            onClick={this.toggleBusy}
-            onFailed={this.toggleBusy}
-            onSuccess={this.onSuccess}
-            params={[gasLimit, wasm]}
-            tx='contract.putCode'
-            ref={this.button}
-          />
-        </Button.Group>
-      </>
-    );
-  }
-
-  private renderExisting () {
-    const { t } = this.props;
-    const { codeHash, isAbiValid, isCodeValid, isNameValid } = this.state;
-    const isValid = isAbiValid && isCodeValid && isNameValid;
-
-    return (
-      <>
-        <Input
-          autoFocus
-          help={t('The code hash for the on-chain deployed code.')}
-          isError={!isCodeValid}
-          label={t('code hash')}
-          onChange={this.onChangeHash}
-          onEnter={this.submit}
-          value={codeHash}
-        />
-        <ValidateCode
-          codeHash={codeHash}
-          onChange={this.onValidateCode}
-        />
-        {this.renderInputName()}
-        {this.renderInputAbi()}
-        <Button.Group>
-          <Button
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Save')}
-            onClick={this.onSave}
-            ref={this.button}
-          />
-        </Button.Group>
-      </>
-    );
-  }
-
-  private renderInputAbi () {
-    const { t } = this.props;
-    const { isAbiValid } = this.state;
-
-    return (
-      <ABI
-        help={t('The ABI for the WASM code. In this step it is optional, but setting it here simplifies the setup of contract instances.')}
-        isError={!isAbiValid}
-        label={t('contract ABI (optional)')}
-        onChange={this.onAddAbi}
-      />
-    );
-  }
-
-  private renderInputName () {
-    const { t } = this.props;
-    const { isNameValid, isNew, name } = this.state;
-
-    return (
-      <Input
-        help={t('A name for this WASM code that helps to user distinguish. Only used for display purposes.')}
-        isError={!isNameValid}
-        label={t('code bundle name')}
-        onChange={this.onChangeName}
-        onEnter={this[isNew ? 'sendTx' : 'submit']}
-        value={name}
-      />
-    );
-  }
-
-  private onAddAbi = (abi: string | null): void => {
-    this.setState({ abi, isAbiValid: !!abi });
-  }
-
-  private onAddWasm = (wasm: Uint8Array, name: string): void => {
-    const isWasmValid = wasm.subarray(0, 4).toString() === '0,97,115,109'; // '\0asm'
-
-    this.setState({ wasm: compactAddLength(wasm), isWasmValid });
-    this.onChangeName(name);
-  }
-
-  private onChangeAccount = (accountId: string | null): void => {
-    this.setState({ accountId });
-  }
-
-  private onChangeGas = (gasLimit: BN | undefined): void => {
-    this.setState({ gasLimit: gasLimit || new BN(0) });
-  }
-
-  private onChangeHash = (codeHash: string): void => {
-    this.setState({ codeHash, isCodeValid: false });
-  }
-
-  private onChangeName = (name: string): void => {
-    this.setState({ name, isNameValid: name.length !== 0 });
-  }
-
-  private onValidateCode = (isCodeValid: boolean): void => {
-    this.setState({ isCodeValid });
-  }
-
-  private toggleBusy = (): void => {
-    this.setState(({ isBusy }) => ({
-      isBusy: !isBusy
-    }));
-  }
-
-  private toggleNew = (): void => {
-    this.setState(({ isNew }) => ({
-      abi: null,
-      codeHash: null,
-      isAbiValid: true,
-      isCodeValid: false,
-      isNameValid: false,
-      name: '',
-      isNew: !isNew
-    }));
-  }
-
-  private onSave = (): void => {
-    const { abi, codeHash, name } = this.state;
-
-    if (!codeHash || !name) {
-      return;
-    }
-
-    store.saveCode(new Hash(codeHash), { abi, name }).catch((error) => {
-      console.error('Unable to save code', error);
-    });
-
-    this.redirect();
-  }
-
-  private onSuccess = (result: SubmittableResult): void => {
-    const record = result.findRecord('contract', 'CodeStored');
-
-    if (record) {
-      const codeHash = record.event.data[0];
-
-      this.setState(({ abi, name }) => {
-        if (!codeHash || !name) {
-          return;
-        }
-
-        store.saveCode(codeHash as Hash, { abi, name }).catch((error) => {
-          console.error('Unable to save code', error);
-        });
-
-        this.redirect();
-      });
-    }
-
-    this.toggleBusy();
-  }
-
-  private redirect () {
-    window.location.hash = store.hasContracts
-      ? `${this.props.basePath}/instantiate`
-      : this.props.basePath;
-  }
-}
-
-export default translate(Deploy);

+ 117 - 0
packages/app-contracts/src/Codes/Add.tsx

@@ -0,0 +1,117 @@
+// Copyright 2017-2019 @polkadot/app-contracts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import React from 'react';
+import { Button, Input } from '@polkadot/ui-app';
+import { Hash } from '@polkadot/types';
+
+import ContractModal, { ContractModalProps, ContractModalState } from '../Modal';
+import ValidateCode from './ValidateCode';
+import store from '../store';
+import translate from '../translate';
+
+type Props = ContractModalProps;
+
+type State = ContractModalState & {
+  codeHash: string,
+  isBusy: boolean,
+  isCodeValid: boolean
+};
+
+class Add extends ContractModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+    this.defaultState = {
+      ...this.defaultState,
+      codeHash: '',
+      isBusy: false,
+      isCodeValid: false
+    };
+    this.state = this.defaultState;
+    this.headerText = props.t('Add an existing code hash');
+  }
+
+  renderContent = () => {
+    const { t } = this.props;
+    const { codeHash, isBusy, isCodeValid } = this.state;
+
+    return (
+      <>
+        <Input
+          autoFocus
+          help={t('The code hash for the on-chain deployed code.')}
+          isDisabled={isBusy}
+          isError={!isCodeValid}
+          label={t('code hash')}
+          onChange={this.onChangeHash}
+          onEnter={this.submit}
+          value={codeHash}
+        />
+        <ValidateCode
+          codeHash={codeHash}
+          onChange={this.onValidateCode}
+        />
+        {this.renderInputName()}
+        {this.renderInputTags()}
+        {this.renderInputAbi()}
+      </>
+    );
+  }
+
+  renderButtons = () => {
+    const { t } = this.props;
+    const { isBusy, isCodeValid, isNameValid } = this.state;
+    const isValid = !isBusy && isCodeValid && isNameValid;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <Button
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Save')}
+          onClick={this.onSave}
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private onChangeHash = (codeHash: string): void => {
+    this.setState({ codeHash, isCodeValid: false });
+  }
+
+  private onValidateCode = (isCodeValid: boolean): void => {
+    this.setState({ isCodeValid });
+  }
+
+  private onSave = (): void => {
+    const { abi, codeHash, name, tags } = this.state;
+
+    if (!codeHash || !name) {
+      return;
+    }
+
+    this.setState(
+      { isBusy: true },
+      () => {
+        store.saveCode(new Hash(codeHash), { abi, name, tags })
+          .then(() => {
+            this.setState(
+              { isBusy: false },
+              () => this.onClose()
+            );
+          })
+          .catch((error) => {
+            console.error('Unable to save code', error);
+            this.setState({ isBusy: false });
+          });
+      }
+    );
+
+    // this.redirect();
+  }
+}
+
+export default translate(Add);

+ 172 - 0
packages/app-contracts/src/Codes/Code.tsx

@@ -0,0 +1,172 @@
+// Copyright 2017-2019 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { CodeStored } from '../types';
+
+import React from 'react';
+import styled from 'styled-components';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import { Button, Card, CodeRow, Forget } from '@polkadot/ui-app';
+import { withMulti } from '@polkadot/ui-api';
+
+import ABI from '../ABI';
+import RemoveABI from '../RemoveABI';
+
+import contracts from '../store';
+import translate from '../translate';
+
+type Props = I18nProps & RouteComponentProps & {
+  basePath: string,
+  code: CodeStored,
+  showDeploy: (codeHash?: string) => () => void
+};
+
+type State = {
+  isForgetOpen: boolean,
+  isRemoveABIOpen: boolean
+};
+
+const CodeCard = styled(Card)`
+  && {
+    min-height: 13rem;
+  }
+`;
+
+class Contract extends React.PureComponent<Props> {
+  state: State = {
+    isForgetOpen: false,
+    isRemoveABIOpen: false
+  };
+
+  render () {
+    const { code, code: { contractAbi } } = this.props;
+
+    return (
+      <CodeCard>
+        {this.renderModals()}
+        <CodeRow
+          buttons={this.renderButtons()}
+          code={code}
+          isEditable
+          withTags
+        >
+          <ABI
+            contractAbi={contractAbi}
+            onChange={this.onChangeABI}
+            onRemove={this.toggleRemoveABI}
+          />
+        </CodeRow>
+      </CodeCard>
+    );
+  }
+
+  private renderButtons () {
+    const { code: { json: { codeHash } }, showDeploy, t } = this.props;
+
+    return (
+      <>
+        <Button
+          isNegative
+          onClick={this.toggleForget}
+          icon='trash'
+          size='small'
+          tooltip={t('Forget this code hash')}
+        />
+        <Button
+          isPrimary
+          label={t('deploy')}
+          labelIcon='cloud upload'
+          onClick={showDeploy(codeHash)}
+          size='small'
+          tooltip={t('Deploy this code hash as a smart contract')}
+        />
+      </>
+    );
+  }
+
+  private renderModals () {
+    const { code } = this.props;
+    const { isForgetOpen, isRemoveABIOpen } = this.state;
+
+    if (!code) {
+      return null;
+    }
+
+    const modals = [];
+
+    if (isForgetOpen) {
+      modals.push(
+        <Forget
+          code={code}
+          key='modal-forget-account'
+          mode='code'
+          onClose={this.toggleForget}
+          onForget={this.onForget}
+        />
+      );
+    }
+
+    if (isRemoveABIOpen) {
+      modals.push(
+        <RemoveABI
+          code={code}
+          key='modal-remove-abi'
+          onClose={this.toggleRemoveABI}
+          onRemove={() => this.onChangeABI(null)}
+        />
+      );
+    }
+
+    return modals;
+  }
+
+  private toggleForget = (): void => {
+    const { isForgetOpen } = this.state;
+
+    this.setState({
+      isForgetOpen: !isForgetOpen
+    });
+  }
+
+  private toggleRemoveABI = (): void => {
+    const { isRemoveABIOpen } = this.state;
+
+    this.setState({
+      isRemoveABIOpen: !isRemoveABIOpen
+    });
+  }
+
+  private onForget = (): void => {
+    const { code: { json: { codeHash } } } = this.props;
+
+    if (!codeHash) {
+      return;
+    }
+
+    try {
+      contracts.forgetCode(codeHash);
+    } catch (error) {
+      console.error(error);
+    } finally {
+      this.toggleForget();
+    }
+  }
+
+  private onChangeABI = async (abi: string | null) => {
+    const { code: { json: { codeHash } } } = this.props;
+
+    await contracts.saveCode(
+      codeHash,
+      { abi }
+    );
+  }
+}
+
+export default withMulti(
+  Contract,
+  translate,
+  withRouter
+);

+ 118 - 0
packages/app-contracts/src/Codes/Upload.tsx

@@ -0,0 +1,118 @@
+// Copyright 2017-2019 @polkadot/app-contracts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { SubmittableResult } from '@polkadot/api/SubmittableExtrinsic';
+
+import BN from 'bn.js';
+import React from 'react';
+import { Button, InputFile, TxButton } from '@polkadot/ui-app';
+import { compactAddLength } from '@polkadot/util';
+import { Hash } from '@polkadot/types';
+
+import ContractModal, { ContractModalProps, ContractModalState } from '../Modal';
+import store from '../store';
+import translate from '../translate';
+
+type Props = ContractModalProps;
+
+type State = ContractModalState & {
+  gasLimit: BN,
+  isWasmValid: boolean,
+  wasm?: Uint8Array | null
+};
+
+class Upload extends ContractModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+    this.defaultState = {
+      ...this.defaultState,
+      isWasmValid: false,
+      wasm: null
+    };
+    this.state = this.defaultState;
+    this.headerText = props.t('Upload WASM');
+  }
+
+  renderContent = () => {
+    const { t } = this.props;
+    const { isBusy, isWasmValid, wasm } = this.state;
+
+    return (
+      <>
+        {this.renderInputAccount()}
+        <InputFile
+          help={t('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
+          isDisabled={isBusy}
+          isError={!isWasmValid}
+          label={t('compiled contract WASM')}
+          onChange={this.onAddWasm}
+          placeholder={
+            wasm && !isWasmValid
+              ? t('The code is not recognized as being in valid WASM format')
+              : null
+          }
+        />
+        {this.renderInputName()}
+        {this.renderInputTags()}
+        {this.renderInputAbi()}
+        {this.renderInputGas()}
+      </>
+    );
+  }
+
+  renderButtons = () => {
+    const { t } = this.props;
+    const { accountId, gasLimit, isBusy, isNameValid, isWasmValid, wasm } = this.state;
+    const isValid = !isBusy && accountId && isNameValid && isWasmValid && !gasLimit.isZero() && !!accountId;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <TxButton
+          accountId={accountId}
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Upload')}
+          onClick={this.toggleBusy(true)}
+          onSuccess={this.onSuccess}
+          onFailed={this.toggleBusy(false)}
+          params={[gasLimit, wasm]}
+          tx='contract.putCode'
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private onAddWasm = (wasm: Uint8Array, name: string): void => {
+    const isWasmValid = wasm.subarray(0, 4).toString() === '0,97,115,109'; // '\0asm'
+
+    this.setState({ wasm: compactAddLength(wasm), isWasmValid });
+    this.onChangeName(name);
+  }
+
+  private onSuccess = (result: SubmittableResult): void => {
+    this.setState(({ abi, name, tags }) => {
+
+      const record = result.findRecord('contract', 'CodeStored');
+
+      if (record) {
+        const codeHash = record.event.data[0];
+
+        if (!codeHash || !name) {
+          return;
+        }
+
+        store.saveCode(codeHash as Hash, { abi, name, tags })
+          .then(() => this.onClose())
+          .catch((error: any) => {
+            console.error('Unable to save code', error);
+          });
+      }
+      return { isBusy: false } as State;
+    });
+  }
+}
+
+export default translate(Upload);

+ 1 - 1
packages/app-contracts/src/ValidateCode.tsx → packages/app-contracts/src/Codes/ValidateCode.tsx

@@ -11,7 +11,7 @@ import { withCalls } from '@polkadot/ui-api';
 import { InfoForInput } from '@polkadot/ui-app';
 import { isHex } from '@polkadot/util';
 
-import translate from './translate';
+import translate from '../translate';
 
 type Props = ApiProps & I18nProps & {
   codeHash?: string | null,

+ 98 - 0
packages/app-contracts/src/Codes/index.tsx

@@ -0,0 +1,98 @@
+// Copyright 2017-2019 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { ComponentProps } from '../types';
+
+import React from 'react';
+import { Button, CardGrid } from '@polkadot/ui-app';
+
+import contracts from '../store';
+import translate from '../translate';
+
+import Code from './Code';
+import Upload from './Upload';
+import Add from './Add';
+
+type Props = ComponentProps & I18nProps;
+
+type State = {
+  isAddOpen: boolean,
+  isUploadOpen: boolean
+};
+
+class Codes extends React.PureComponent<Props> {
+  state = {
+    isAddOpen: false,
+    isUploadOpen: false
+  } as State;
+
+  render () {
+    const { basePath, showDeploy, t } = this.props;
+    const { isAddOpen, isUploadOpen } = this.state;
+
+    return (
+      <>
+        <CardGrid
+          emptyText={t('No code hashes available')}
+          buttons={
+            <Button.Group>
+              <Button
+                isPrimary
+                label={t('Upload WASM')}
+                labelIcon='upload'
+                onClick={this.showUpload}
+              />
+              <Button.Or />
+              <Button
+                label={t('Add an existing code hash')}
+                labelIcon='add'
+                onClick={this.showAdd}
+              />
+            </Button.Group>
+          }
+        >
+          {contracts.getAllCode().map((code) => {
+            return (
+              <Code
+                key={code.json.codeHash}
+                code={code}
+                showDeploy={showDeploy}
+              />
+            );
+          })}
+        </CardGrid>
+        <Upload
+          basePath={basePath}
+          isNew
+          onClose={this.hideUpload}
+          isOpen={isUploadOpen}
+        />
+        <Add
+          basePath={basePath}
+          onClose={this.hideAdd}
+          isOpen={isAddOpen}
+        />
+      </>
+    );
+  }
+
+  private showUpload = () => {
+    this.setState({ isUploadOpen: true });
+  }
+
+  private hideUpload = () => {
+    this.setState({ isUploadOpen: false });
+  }
+
+  private showAdd = () => {
+    this.setState({ isAddOpen: true });
+  }
+
+  private hideAdd = () => {
+    this.setState({ isAddOpen: false });
+  }
+}
+
+export default translate(Codes);

+ 133 - 0
packages/app-contracts/src/Contracts/Add.tsx

@@ -0,0 +1,133 @@
+// Copyright 2017-2019 @polkadot/app-contracts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { ActionStatus } from '@polkadot/ui-app/Status/types';
+
+import React from 'react';
+import { api } from '@polkadot/ui-api';
+import { AddressRow, Button, Input } from '@polkadot/ui-app';
+import keyring from '@polkadot/ui-keyring';
+
+import ContractModal, { ContractModalProps, ContractModalState } from '../Modal';
+import ValidateAddr from './ValidateAddr';
+
+import translate from '../translate';
+
+type Props = ContractModalProps & I18nProps;
+
+type State = ContractModalState & {
+  address?: string | null,
+  isAddressValid: boolean
+};
+
+class Add extends ContractModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+    this.defaultState = {
+      ...this.defaultState,
+      address: null,
+      name: 'New Contract',
+      isAddressValid: false,
+      isNameValid: true
+    };
+    this.state = this.defaultState;
+    this.headerText = props.t('Add an existing contract');
+  }
+
+  isContract = true;
+
+  renderContent = () => {
+    const { t } = this.props;
+    const { address, isAddressValid, isBusy, name } = this.state;
+
+    return (
+      <AddressRow
+        defaultName={name}
+        isValid
+        value={address || null}
+      >
+        <Input
+          autoFocus
+          help={t('The address for the deployed contract instance.')}
+          isDisabled={isBusy}
+          isError={!isAddressValid}
+          label={t('contract address')}
+          onChange={this.onChangeAddress}
+          onEnter={this.submit}
+          value={address || ''}
+        />
+        <ValidateAddr
+          address={address}
+          onChange={this.onValidateAddr}
+        />
+        {this.renderInputName()}
+        {this.renderInputTags()}
+        {this.renderInputAbi()}
+      </AddressRow>
+    );
+  }
+
+  renderButtons = () => {
+    const { t } = this.props;
+    const { isAddressValid, isAbiValid, isNameValid } = this.state;
+    const isValid = isNameValid && isAddressValid && isAbiValid;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <Button
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Save')}
+          onClick={this.onAdd}
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private onChangeAddress = (address: string): void => {
+    this.setState({ address, isAddressValid: false });
+  }
+
+  private onValidateAddr = (isAddressValid: boolean): void => {
+    this.setState({ isAddressValid });
+  }
+
+  private onAdd = async () => {
+    await api.isReady;
+    const status = { action: 'create' } as ActionStatus;
+    const { address, abi, name, tags } = this.state;
+
+    if (!address || !abi || !name) {
+      return;
+    }
+
+    try {
+      const json = {
+        name,
+        tags,
+        contract: {
+          abi,
+          genesisHash: api.genesisHash.toHex()
+        }
+      };
+
+      keyring.saveContract(address, json);
+
+      status.account = address;
+      status.status = address ? 'success' : 'error';
+      status.message = 'contract added';
+      this.onClose();
+
+    } catch (error) {
+      console.error(error);
+      status.status = 'error';
+      status.message = error.message;
+    }
+  }
+}
+
+export default translate(Add);

+ 170 - 50
packages/app-contracts/src/Call.tsx → packages/app-contracts/src/Contracts/Call.tsx

@@ -2,49 +2,114 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { I18nProps } from '@polkadot/ui-app/types';
-import { ComponentProps } from './types';
+import { BareProps, I18nProps } from '@polkadot/ui-app/types';
 
 import BN from 'bn.js';
 import React from 'react';
-import { Button, Dropdown, InputAddress, InputBalance, InputNumber, TxButton, TxComponent } from '@polkadot/ui-app';
+import { withRouter } from 'react-router-dom';
+import { Button, Dropdown, InputAddress, InputBalance, InputNumber, Modal, TxButton, TxComponent } from '@polkadot/ui-app';
+import { getContractAbi } from '@polkadot/ui-app/util';
+import { withMulti } from '@polkadot/ui-api';
 import { ContractAbi } from '@polkadot/types';
 
-import store from './store';
-import translate from './translate';
-import Params from './Params';
+import translate from '../translate';
+import Params from '../Params';
 
-type Props = ComponentProps & I18nProps;
+type Props = BareProps & I18nProps & {
+  address: string | null,
+  isOpen: boolean,
+  method: string | null
+  onClose: () => void
+};
 
 type State = {
   accountId: string | null,
-  address?: string,
+  address: string | null,
   contractAbi?: ContractAbi | null,
   endowment: BN,
   gasLimit: BN,
   isAddressValid: boolean,
   isBusy: boolean,
-  method?: string | null,
+  method: string | null,
   params: Array<any>
 };
 
 class Call extends TxComponent<Props, State> {
-  state: State = {
+  defaultState: State = {
+    address: null,
     accountId: null,
     endowment: new BN(0),
     gasLimit: new BN(0),
+    method: null,
     isAddressValid: false,
     isBusy: false,
     params: []
   };
 
+  state: State = this.defaultState;
+
+  static getDerivedStateFromProps ({ address: propsAddress, method: propsMethod, isOpen }: Props, { address, method }: State) {
+    if (!isOpen) {
+      return {
+        address: null,
+        method: null,
+        contractAbi: null,
+        isValidAddress: false
+      };
+    }
+    return {
+      ...(
+        !address
+          ? {
+            address: propsAddress,
+            contractAbi: propsAddress ? getContractAbi(propsAddress) : null,
+            isValidAddress: !!propsAddress
+          }
+          : {}
+      ),
+      ...(
+        !method
+          ? { method: propsMethod }
+          : {}
+      )
+    };
+  }
+
   render () {
+    const { isOpen, t } = this.props;
+
+    return (
+      <Modal
+        className='app--contracts-Modal'
+        dimmer='inverted'
+        onClose={this.onClose}
+        open={isOpen}
+      >
+        <Modal.Header>
+          {t('Call a contract')}
+        </Modal.Header>
+        <Modal.Content>
+          {this.renderContent()}
+        </Modal.Content>
+        <Modal.Actions>
+          {this.renderButtons()}
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  renderContent = () => {
     const { t } = this.props;
-    const { accountId, address, contractAbi, gasLimit, isAddressValid, method } = this.state;
-    const contractOptions = store.getAllContracts().map(({ json: { address, name } }) => ({
-      text: `${name} (${address})`,
-      value: address
-    }));
+    const { gasLimit } = this.state;
+
+    const [ address, contractAbi, method ] = this.getCallProps();
+    const isEndowValid = true;
+    const isGasValid = !gasLimit.isZero();
+
+    if (!address || !contractAbi) {
+      return null;
+    }
+
     const methodOptions = contractAbi
       ? Object.keys(contractAbi.messages).map((key) => {
         const fn = contractAbi.messages[key];
@@ -59,12 +124,6 @@ class Call extends TxComponent<Props, State> {
         };
       })
       : [];
-    const defaultContract = contractOptions.length
-      ? contractOptions[contractOptions.length - 1].value
-      : undefined;
-    const isEndowValid = true; // !endowment.isZero();
-    const isGasValid = !gasLimit.isZero();
-    const isValid = !!accountId && isEndowValid && isGasValid && isAddressValid;
 
     return (
       <div className='contracts--Call'>
@@ -74,13 +133,11 @@ class Call extends TxComponent<Props, State> {
           onChange={this.onChangeAccount}
           type='account'
         />
-        <Dropdown
-          defaultValue={defaultContract}
+        <InputAddress
           help={t('A deployed contract that has either been deployed or attached. The address and ABI are used to construct the parameters.')}
-          isError={!isAddressValid}
           label={t('contract to use')}
           onChange={this.onChangeAddress}
-          options={contractOptions}
+          type='contract'
           value={address}
         />
         <Dropdown
@@ -90,6 +147,7 @@ class Call extends TxComponent<Props, State> {
           label={t('message to send')}
           onChange={this.onChangeMethod}
           options={methodOptions}
+          style={{ fontFamily: 'monospace' }}
           value={method}
         />
         <Params
@@ -114,26 +172,73 @@ class Call extends TxComponent<Props, State> {
           onChange={this.onChangeGas}
           onEnter={this.sendTx}
         />
-        <Button.Group>
-          <TxButton
-            accountId={accountId}
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Call')}
-            onClick={this.toggleBusy}
-            onFailed={this.toggleBusy}
-            onSuccess={this.toggleBusy}
-            params={this.constructCall}
-            tx='contract.call'
-            ref={this.button}
-          />
-        </Button.Group>
       </div>
     );
   }
 
+  private renderButtons = () => {
+    const { t } = this.props;
+    const { accountId, gasLimit, isAddressValid } = this.state;
+    const isEndowValid = true; // !endowment.isZero();
+    const isGasValid = !gasLimit.isZero();
+    const isValid = !!accountId && isEndowValid && isGasValid && isAddressValid;
+
+    return (
+      <Button.Group>
+        <Button
+          isNegative
+          onClick={this.onClose}
+          label={t('Cancel')}
+        />
+        <Button.Or />
+        <TxButton
+          accountId={accountId}
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Call')}
+          onClick={this.toggleBusy}
+          onFailed={this.toggleBusy}
+          onSuccess={this.toggleBusy}
+          params={this.constructCall}
+          tx='contract.call'
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private getCallProps = (): [string | null, ContractAbi | null, string | null] => {
+    let address;
+    let contractAbi;
+    let method;
+
+    if (!this.state.address) {
+      return [null, null, null];
+    } else {
+      address = this.state.address;
+      contractAbi = this.state.contractAbi || getContractAbi(address);
+      method = contractAbi && this.state.method && contractAbi.messages[this.state.method] ?
+        this.state.method :
+        (
+          contractAbi ?
+            Object.keys(contractAbi.messages)[0] :
+            null
+        );
+    }
+
+    return [
+      address || null,
+      contractAbi || null,
+      method || null
+    ];
+  }
+
   private constructCall = (): Array<any> => {
-    const { address, contractAbi, endowment, gasLimit, method, params } = this.state;
+    const {
+      endowment, gasLimit, params
+    } = this.state;
+
+    const [ address, contractAbi, method ] = this.getCallProps();
 
     if (!contractAbi || !method) {
       return [];
@@ -147,17 +252,9 @@ class Call extends TxComponent<Props, State> {
   }
 
   private onChangeAddress = (address: string): void => {
-    const contract = store.getContract(address);
-    const contractAbi = contract
-    ? contract.contractAbi
-    : null;
+    const contractAbi = getContractAbi(address);
 
     this.setState({ address, contractAbi, isAddressValid: !!contractAbi });
-    this.onChangeMethod(
-      contractAbi
-        ? Object.keys(contractAbi.messages)[0]
-        : null
-    );
   }
 
   private onChangeEndowment = (endowment?: BN | null): void => {
@@ -181,6 +278,29 @@ class Call extends TxComponent<Props, State> {
       isBusy: !isBusy
     }));
   }
+
+  private reset = () => {
+    this.setState((state: State) => {
+      if (!state.isBusy) {
+        return {
+          ...state,
+          ...this.defaultState
+        };
+      }
+      return {} as State;
+    });
+  }
+
+  private onClose = () => {
+    const { onClose } = this.props;
+
+    this.reset();
+    onClose && onClose();
+  }
 }
 
-export default translate(Call);
+export default withMulti(
+  Call,
+  translate,
+  withRouter
+);

+ 160 - 0
packages/app-contracts/src/Contracts/Contract.tsx

@@ -0,0 +1,160 @@
+// Copyright 2017-2019 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { ActionStatus } from '@polkadot/ui-app/Status/types';
+import { I18nProps } from '@polkadot/ui-app/types';
+
+import React from 'react';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import keyring from '@polkadot/ui-keyring';
+import { AddressRow, Button, Card, Forget, Messages } from '@polkadot/ui-app';
+import { getContractAbi } from '@polkadot/ui-app/util';
+import { withMulti } from '@polkadot/ui-api';
+
+import translate from '../translate';
+
+type Props = I18nProps & RouteComponentProps & {
+  basePath: string,
+  address: string,
+  onCall: (callAddress?: string, callMethod?: string) => void
+};
+
+type State = {
+  isBackupOpen: boolean,
+  isForgetOpen: boolean,
+  isPasswordOpen: boolean
+};
+
+class Contract extends React.PureComponent<Props> {
+  state: State;
+
+  constructor (props: Props) {
+    super(props);
+    this.state = {
+      isBackupOpen: false,
+      isForgetOpen: false,
+      isPasswordOpen: false
+    };
+  }
+
+  render () {
+    const { address, onCall } = this.props;
+
+    const contractAbi = getContractAbi(address);
+
+    if (!contractAbi) {
+      return null;
+    }
+
+    return (
+      <Card>
+        {this.renderModals()}
+        <AddressRow
+          buttons={this.renderButtons()}
+          isContract
+          isEditable
+          type='contract'
+          value={address}
+          withBalance={false}
+          withNonce={false}
+          withTags
+        >
+          <Messages
+            address={address}
+            contractAbi={contractAbi}
+            isRemovable={false}
+            onSelect={onCall}
+          />
+        </AddressRow>
+      </Card>
+    );
+  }
+
+  private renderModals () {
+    const { address } = this.props;
+    const { isForgetOpen } = this.state;
+
+    if (!address) {
+      return null;
+    }
+
+    const modals = [];
+
+    if (isForgetOpen) {
+      modals.push(
+        <Forget
+          address={address}
+          mode='contract'
+          onForget={this.onForget}
+          key='modal-forget-contract'
+          onClose={this.toggleForget}
+        />
+      );
+    }
+
+    return modals;
+  }
+
+  private toggleForget = (): void => {
+    const { isForgetOpen } = this.state;
+
+    this.setState({
+      isForgetOpen: !isForgetOpen
+    });
+  }
+
+  private onForget = (): void => {
+    const { address, t } = this.props;
+
+    if (!address) {
+      return;
+    }
+
+    const status = {
+      account: address,
+      action: 'forget'
+    } as ActionStatus;
+
+    try {
+      keyring.forgetContract(address);
+      status.status = 'success';
+      status.message = t('address forgotten');
+    } catch (error) {
+      status.status = 'error';
+      status.message = error.message;
+    }
+    this.toggleForget();
+  }
+
+  private renderButtons () {
+    const { address, onCall, t } = this.props;
+
+    return (
+      <div className='contracts--Contract-buttons'>
+        <Button
+          isNegative
+          onClick={this.toggleForget}
+          icon='trash'
+          size='small'
+          tooltip={t('Forget this contract')}
+        />
+        <Button
+          isPrimary
+          label={t('execute')}
+          labelIcon='play'
+          onClick={() => onCall(address)}
+          size='small'
+          tooltip={t('Call a method on this contract')}
+        />
+      </div>
+    );
+  }
+}
+
+export default withMulti(
+  Contract,
+  translate,
+  withRouter
+);

+ 9 - 6
packages/app-contracts/src/ValidateAddr.tsx → packages/app-contracts/src/Contracts/ValidateAddr.tsx

@@ -6,16 +6,16 @@ import { I18nProps } from '@polkadot/ui-app/types';
 import { ApiProps } from '@polkadot/ui-api/types';
 
 import React from 'react';
-import { CodeHash, Option } from '@polkadot/types';
+import { ContractInfo, Option } from '@polkadot/types';
 import { withCalls } from '@polkadot/ui-api';
 import { InfoForInput } from '@polkadot/ui-app';
 import keyring from '@polkadot/ui-keyring';
 
-import translate from './translate';
+import translate from '../translate';
 
 type Props = ApiProps & I18nProps & {
   address?: string | null,
-  contract_codeHashOf?: Option<CodeHash>,
+  contract_contractInfoOf?: Option<ContractInfo>,
   onChange: (isValid: boolean) => void
 };
 
@@ -32,7 +32,7 @@ class ValidateAddr extends React.PureComponent<Props> {
     isValid: false
   };
 
-  static getDerivedStateFromProps ({ address, contract_codeHashOf, onChange }: Props): State {
+  static getDerivedStateFromProps ({ address, contract_contractInfoOf, onChange }: Props): State {
     let isValidAddr = false;
 
     try {
@@ -43,7 +43,10 @@ class ValidateAddr extends React.PureComponent<Props> {
       // ignore
     }
 
-    const isStored = !!contract_codeHashOf && contract_codeHashOf.isSome;
+    const isStored = (
+      (!!contract_contractInfoOf && contract_contractInfoOf.isSome)
+      // (!!contract_codeHashOf && contract_codeHashOf.isSome)
+    );
     const isValid = isValidAddr && isStored;
 
     // FIXME Really not convinced this is the correct place to do this type of callback?
@@ -78,6 +81,6 @@ class ValidateAddr extends React.PureComponent<Props> {
 
 export default translate(
   withCalls<Props>(
-    ['query.contract.codeHashOf', { paramName: 'address' }]
+    ['query.contract.contractInfoOf', { paramName: 'address' }]
   )(ValidateAddr)
 );

+ 124 - 0
packages/app-contracts/src/Contracts/index.tsx

@@ -0,0 +1,124 @@
+// Copyright 2017-2019 @polkadot/app-staking authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { ComponentProps } from '../types';
+
+import React from 'react';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+import { Button, CardGrid } from '@polkadot/ui-app';
+import { withMulti } from '@polkadot/ui-api';
+
+import translate from '../translate';
+import Add from './Add';
+import Contract from './Contract';
+import Call from './Call';
+
+type Props = ComponentProps & I18nProps & RouteComponentProps;
+
+type State = {
+  isAddOpen: boolean,
+  isCallOpen: boolean,
+  callAddress: string | null,
+  callMethod: string | null
+};
+
+class Contracts extends React.PureComponent<Props, State> {
+  state: State = {
+    callAddress: null,
+    callMethod: null,
+    isAddOpen: false,
+    isCallOpen: false
+  };
+
+  render () {
+    const { accounts, basePath, contracts, hasCode, showDeploy, t } = this.props;
+    const { callAddress, callMethod, isAddOpen, isCallOpen } = this.state;
+
+    return (
+      <>
+        <CardGrid
+          emptyText={t('No contracts available')}
+          buttons={
+            <Button.Group>
+              {hasCode && (
+                <>
+                  <Button
+                    isPrimary
+                    label={t('Deploy a code hash')}
+                    labelIcon='cloud upload'
+                    onClick={showDeploy()}
+                  />
+                  <Button.Or />
+                </>
+              )}
+              <Button
+                label={t('Add an existing contract')}
+                labelIcon='add'
+                onClick={this.showAdd}
+              />
+            </Button.Group>
+          }
+        >
+          {accounts && contracts && Object.keys(contracts).map((address) => {
+            return (
+              <Contract
+                basePath={basePath}
+                address={address}
+                key={address}
+                onCall={this.showCall}
+              />
+            );
+          })}
+        </CardGrid>
+        <Add
+          basePath={basePath}
+          isOpen={isAddOpen}
+          onClose={this.hideAdd}
+        />
+        <Call
+          address={callAddress}
+          isOpen={isCallOpen}
+          method={callMethod}
+          onClose={this.hideCall}
+        />
+      </>
+    );
+  }
+
+  private showAdd = () => {
+    this.setState({
+      isAddOpen: true
+    });
+  }
+
+  private hideAdd = () => {
+    this.setState({
+      isAddOpen: false
+    });
+  }
+
+  private showCall = (callAddress?: string, callMethod?: string) => {
+    this.setState({
+      isCallOpen: true,
+      callAddress: callAddress || null,
+      callMethod: callMethod || null
+    });
+  }
+
+  private hideCall = () => {
+    this.setState({
+      isCallOpen: false,
+      callAddress: null,
+      callMethod: null
+    });
+  }
+}
+
+export default withMulti(
+  Contracts,
+  translate,
+  withRouter
+);

+ 290 - 0
packages/app-contracts/src/Deploy.tsx

@@ -0,0 +1,290 @@
+// Copyright 2017-2019 @polkadot/app-contracts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { SubmittableResult } from '@polkadot/api/SubmittableExtrinsic';
+import { I18nProps } from '@polkadot/ui-app/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { RouteComponentProps } from 'react-router';
+import { withRouter } from 'react-router-dom';
+
+import { api, withMulti } from '@polkadot/ui-api';
+import keyring from '@polkadot/ui-keyring';
+import { Button, Dropdown, InputBalance, TxButton } from '@polkadot/ui-app';
+import { AccountId, ContractAbi, getTypeDef } from '@polkadot/types';
+import createValues from '@polkadot/ui-params/values';
+
+import ContractModal, { ContractModalProps, ContractModalState } from './Modal';
+import Params from './Params';
+import store from './store';
+import translate from './translate';
+
+type ConstructOptions = Array<{key: string, text: string, value: string}>;
+
+type Props = ContractModalProps & I18nProps & RouteComponentProps & {
+  codeHash?: string
+};
+
+type State = ContractModalState & {
+  codeHash?: string,
+  constructOptions: ConstructOptions,
+  endowment: BN,
+  isHashValid: boolean,
+  params: Array<any>
+};
+
+class Deploy extends ContractModal<Props, State> {
+  constructor (props: Props) {
+    super(props);
+    this.defaultState = {
+      ...this.defaultState,
+      constructOptions: [],
+      endowment: new BN(0),
+      isHashValid: false,
+      params: [],
+      ...this.getCodeState(props.codeHash)
+    };
+    this.state = this.defaultState;
+  }
+
+  componentWillReceiveProps (nextProps: Props, nextState: State) {
+    super.componentWillReceiveProps(nextProps, nextState);
+
+    if (nextProps.codeHash && nextProps.codeHash !== this.props.codeHash) {
+      this.setState(
+        this.getCodeState(nextProps.codeHash)
+      );
+    }
+
+  }
+
+  isContract = true;
+
+  headerText = 'Deploy a new contract';
+
+  renderContent = () => {
+    const { t } = this.props;
+    const { codeHash, constructOptions, contractAbi, endowment, isAbiSupplied, isBusy, isHashValid } = this.state;
+
+    const isEndowValid = !endowment.isZero();
+    const codeOptions = store.getAllCode().map(({ json: { codeHash, name } }) => ({
+      text: `${name} (${codeHash})`,
+      value: codeHash
+    }));
+
+    const defaultCode = codeOptions.length
+      ? codeOptions[codeOptions.length - 1].value
+      : undefined;
+
+    return (
+      <>
+        {this.renderInputAccount()}
+        <Dropdown
+          defaultValue={defaultCode}
+          help={t('The contract WASM previously deployed. Internally this is identified by the hash of the code, as either created or attached.')}
+          isDisabled={isBusy}
+          isError={!isHashValid}
+          label={t('code for this contract')}
+          onChange={this.onChangeCode}
+          options={codeOptions}
+          value={codeHash}
+        />
+        {this.renderInputName()}
+        {this.renderInputTags()}
+        {
+          isAbiSupplied
+            ? null
+            : this.renderInputAbi()
+        }
+        {
+          contractAbi
+            ? (
+              <Dropdown
+                defaultValue='deploy'
+                help={t('The deployment constructor information for this contract, as provided by the ABI.')}
+                isDisabled
+                label={t('constructor')}
+                options={constructOptions}
+                style={{ fontFamily: 'monospace' }}
+                value='deploy'
+              />
+            )
+            : null
+        }
+        <Params
+          isDisabled={isBusy}
+          onChange={this.onChangeParams}
+          onEnter={this.sendTx}
+          params={
+            contractAbi
+              ? contractAbi.deploy.args
+              : []
+          }
+        />
+        <InputBalance
+          defaultValue={endowment}
+          help={t('The allotted endownment for this contract, i.e. the amount transferred to the contract upon instantiation.')}
+          isDisabled={isBusy}
+          isError={!isEndowValid}
+          label={t('endowment')}
+          onChange={this.onChangeEndowment}
+          onEnter={this.sendTx}
+          value={endowment}
+        />
+        {this.renderInputGas()}
+      </>
+    );
+  }
+
+  renderButtons = () => {
+    const { t } = this.props;
+    const { accountId, endowment, gasLimit, isAbiValid, isHashValid, isNameValid } = this.state;
+    const isEndowValid = !endowment.isZero();
+    const isGasValid = !gasLimit.isZero();
+    const isValid = isAbiValid && isHashValid && isEndowValid && isGasValid && !!accountId && isNameValid;
+
+    return (
+      <Button.Group>
+        {this.renderCancelButton()}
+        <TxButton
+          accountId={accountId}
+          isDisabled={!isValid}
+          isPrimary
+          label={t('Deploy')}
+          onClick={this.toggleBusy(true)}
+          onFailed={this.toggleBusy(false)}
+          onSuccess={this.onSuccess}
+          params={this.constructCall}
+          tx='contract.create'
+          ref={this.button}
+        />
+      </Button.Group>
+    );
+  }
+
+  private getAbiState = (abi: string | null | undefined, contractAbi: ContractAbi | null = null): State => {
+
+    if (contractAbi) {
+      const args = contractAbi.deploy.args.map(({ name, type }) => name + ': ' + type);
+      const text = `deploy(${args.join(', ')})`;
+
+      return {
+        abi,
+        constructOptions: [{
+          key: 'deploy',
+          text,
+          value: 'deploy'
+        }],
+        contractAbi,
+        isAbiValid: !!contractAbi,
+        params: createValues(
+          contractAbi.deploy.args.map(({ name, type }) => ({
+            type: getTypeDef(type, name)
+          }))
+        )
+      } as State;
+    } else {
+      return {
+        constructOptions: [] as ConstructOptions,
+        abi: null,
+        contractAbi: null,
+        isAbiSupplied: false,
+        isAbiValid: false,
+        params: [] as Array<any>
+      } as State;
+    }
+  }
+
+  private getCodeState = (codeHash: string | null = null): State => {
+
+    if (codeHash) {
+      const code = store.getCode(codeHash);
+
+      if (code) {
+        const { contractAbi, json } = code;
+
+        return {
+          codeHash,
+          isAbiSupplied: !!contractAbi,
+          name: `${json.name} (instance)`,
+          isHashValid: true,
+          isNameValid: true,
+          ...this.getAbiState(json.abi, contractAbi)
+        } as State;
+      }
+    }
+
+    return {} as State;
+  }
+
+  private constructCall = (): Array<any> => {
+    const { codeHash, contractAbi, endowment, gasLimit, params } = this.state;
+
+    if (!contractAbi) {
+      return [];
+    }
+
+    return [endowment, gasLimit, codeHash, contractAbi.deploy(...params)];
+  }
+
+  protected onAddAbi = (abi: string | null | undefined, contractAbi?: ContractAbi | null): void => {
+    this.setState({
+      ...this.getAbiState(abi, contractAbi)
+    });
+  }
+
+  private onChangeCode = (codeHash: string): void => {
+    this.setState(
+      this.getCodeState(codeHash)
+    );
+  }
+
+  private onChangeEndowment = (endowment?: BN | null): void => {
+    this.setState({ endowment: endowment || new BN(0) });
+  }
+
+  private onChangeParams = (params: Array<any>): void => {
+    this.setState({ params });
+  }
+
+  private onSuccess = async (result: SubmittableResult) => {
+    const { history } = this.props;
+
+    const record = result.findRecord('contract', 'Instantiated');
+
+    if (record) {
+      const address = record.event.data[1] as any as AccountId;
+
+      await api.isReady;
+
+      this.setState(({ abi, name, tags }) => {
+        if (!abi || !name) {
+          return;
+        }
+
+        keyring.saveContract(address.toString(), {
+          name,
+          contract: {
+            abi,
+            genesisHash: api.genesisHash.toHex()
+          },
+          tags
+        });
+
+        history.push(this.props.basePath);
+
+        this.onClose();
+        return { isBusy: false } as State;
+      });
+    }
+
+  }
+}
+
+export default withMulti(
+  Deploy,
+  translate,
+  withRouter
+);

+ 0 - 371
packages/app-contracts/src/Instantiate.tsx

@@ -1,371 +0,0 @@
-// Copyright 2017-2019 @polkadot/app-contracts authors & contributors
-// This software may be modified and distributed under the terms
-// of the Apache-2.0 license. See the LICENSE file for details.
-
-import { SubmittableResult } from '@polkadot/api/SubmittableExtrinsic';
-import { I18nProps } from '@polkadot/ui-app/types';
-import { ComponentProps } from './types';
-
-import BN from 'bn.js';
-import React from 'react';
-import { Button, Dropdown, Input, InputAddress, InputBalance, InputNumber, TxButton, TxComponent } from '@polkadot/ui-app';
-import { AccountId, ContractAbi } from '@polkadot/types';
-
-import ABI from './ABI';
-import Params from './Params';
-import ValidateAddr from './ValidateAddr';
-import store from './store';
-import translate from './translate';
-
-type Props = ComponentProps & I18nProps;
-
-type State = {
-  abi?: string | null,
-  accountId: string | null,
-  address?: string | null,
-  codeHash?: string,
-  contractAbi?: ContractAbi | null,
-  endowment: BN,
-  gasLimit: BN,
-  isAbiValid: boolean,
-  isAbiSupplied: boolean,
-  isAddressValid: boolean,
-  isBusy: boolean,
-  isHashValid: boolean,
-  isNameValid: boolean,
-  isNew?: boolean,
-  name?: string | null,
-  params: Array<any>
-};
-
-class Create extends TxComponent<Props, State> {
-  state: State = {
-    accountId: null,
-    endowment: new BN(0),
-    gasLimit: new BN(0),
-    isAbiValid: false,
-    isAbiSupplied: false,
-    isAddressValid: false,
-    isBusy: false,
-    isHashValid: false,
-    isNameValid: false,
-    isNew: true,
-    params: []
-  };
-
-  render () {
-    const { t } = this.props;
-    const { isNew } = this.state;
-
-    return (
-      <div className='contracts--Instantiate'>
-        <Button.Group isBasic isCentered>
-          <Button
-            isBasic
-            isNegative={isNew}
-            label={t('deploy new')}
-            onClick={this.toggleNew}
-          />
-          <Button
-            isBasic
-            isNegative={!isNew}
-            label={t('attach existing')}
-            onClick={this.toggleNew}
-          />
-        </Button.Group>
-        {
-          isNew
-            ? this.renderDeploy()
-            : this.renderExisting()
-        }
-      </div>
-    );
-  }
-
-  private renderDeploy () {
-    const { t } = this.props;
-    const { accountId, codeHash, contractAbi, endowment, gasLimit, isAbiSupplied, isAbiValid, isHashValid, isNameValid } = this.state;
-    const isEndowValid = !endowment.isZero();
-    const isGasValid = !gasLimit.isZero();
-    const isValid = isAbiValid && isHashValid && isEndowValid && isGasValid && !!accountId && isNameValid;
-    const codeOptions = store.getAllCode().map(({ json: { codeHash, name } }) => ({
-      text: `${name} (${codeHash})`,
-      value: codeHash
-    }));
-    const defaultCode = codeOptions.length
-      ? codeOptions[codeOptions.length - 1].value
-      : undefined;
-    const constructOptions = contractAbi
-      ? (() => {
-        const args = contractAbi.deploy.args.map(({ name, type }) => name + ': ' + type);
-        const text = `deploy(${args.join(', ')})`;
-
-        return [{
-          key: 'deploy',
-          text,
-          value: 'deploy'
-        }];
-      })()
-      : [];
-
-    return (
-      <>
-        <InputAddress
-          help={t('Specify the user account to use for this contract creation. And fees will be deducted from this account.')}
-          label={t('deployment account')}
-          onChange={this.onChangeAccount}
-          type='account'
-        />
-        <Dropdown
-          defaultValue={defaultCode}
-          help={t('The contract WASM previously deployed. Internally this is identified by the hash of the code, as either created or attached.')}
-          isError={!isHashValid}
-          label={t('code for this contract')}
-          onChange={this.onChangeCode}
-          options={codeOptions}
-          value={codeHash}
-        />
-        {this.renderInputName()}
-        {
-          isAbiSupplied
-            ? null
-            : this.renderInputAbi()
-        }
-        {
-          contractAbi
-            ? (
-              <Dropdown
-                defaultValue='deploy'
-                help={t('The deployment constructor information for this contract, as provided by the ABI.')}
-                isDisabled
-                label={t('constructor')}
-                options={constructOptions}
-                value='deploy'
-              />
-            )
-            : null
-        }
-        <Params
-          onChange={this.onChangeParams}
-          onEnter={this.sendTx}
-          params={
-            contractAbi
-              ? contractAbi.deploy.args
-              : undefined
-          }
-        />
-        <InputBalance
-          help={t('The allotted endownment for this contract, i.e. the amount transferred to the contract upon instantiation.')}
-          isError={!isEndowValid}
-          label={t('endowment')}
-          onChange={this.onChangeEndowment}
-          onEnter={this.sendTx}
-        />
-        <InputNumber
-          help={t('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
-          isError={!isGasValid}
-          label={t('maximum gas allowed')}
-          onChange={this.onChangeGas}
-          onEnter={this.sendTx}
-        />
-        <Button.Group>
-          <TxButton
-            accountId={accountId}
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Instantiate')}
-            onClick={this.toggleBusy}
-            onFailed={this.toggleBusy}
-            onSuccess={this.onSuccess}
-            params={this.constructCall}
-            tx='contract.create'
-            ref={this.button}
-          />
-        </Button.Group>
-      </>
-    );
-  }
-
-  private renderExisting () {
-    const { t } = this.props;
-    const { address, isAddressValid, isAbiValid, isNameValid } = this.state;
-    const isValid = isNameValid && isAddressValid && isAbiValid;
-
-    return (
-      <>
-        <Input
-          autoFocus
-          help={t('The address for the deployed contract instance.')}
-          isError={!isAddressValid}
-          label={t('contract address')}
-          onChange={this.onChangeAddress}
-          onEnter={this.submit}
-          value={address}
-        />
-        <ValidateAddr
-          address={address}
-          onChange={this.onValidateAddr}
-        />
-        {this.renderInputName()}
-        {this.renderInputAbi()}
-        <Button.Group>
-          <Button
-            isDisabled={!isValid}
-            isPrimary
-            label={t('Save')}
-            onClick={this.onSave}
-            ref={this.button}
-          />
-        </Button.Group>
-      </>
-    );
-  }
-
-  private renderInputAbi () {
-    const { t } = this.props;
-    const { isAbiValid } = this.state;
-
-    return (
-      <ABI
-        help={t('The ABI for the WASM code. Since we will be making a call into the code, the ABI is required and stored for future operations such as sending messages.')}
-        isError={!isAbiValid}
-        label={t('Contract ABI')}
-        onChange={this.onAddAbi}
-      />
-    );
-  }
-
-  private renderInputName () {
-    const { t } = this.props;
-    const { isNameValid, isNew, name } = this.state;
-
-    return (
-      <Input
-        help={t('A name for the deployed contract to help you distinguish. Only used for display purposes.')}
-        isError={!isNameValid}
-        label={t('contract name')}
-        onChange={this.onChangeName}
-        onEnter={this[isNew ? 'sendTx' : 'submit']}
-        value={name}
-      />
-    );
-  }
-
-  private constructCall = (): Array<any> => {
-    const { codeHash, contractAbi, endowment, gasLimit, params } = this.state;
-
-    if (!contractAbi) {
-      return [];
-    }
-
-    return [endowment, gasLimit, codeHash, contractAbi.deploy(...params)];
-  }
-
-  private onAddAbi = (abi: string | null | undefined, contractAbi: ContractAbi | null, isAbiSupplied: boolean = false): void => {
-    this.setState({ abi, contractAbi, isAbiSupplied, isAbiValid: !!abi });
-  }
-
-  private onChangeAccount = (accountId: string | null): void => {
-    this.setState({ accountId });
-  }
-
-  private onChangeAddress = (address: string): void => {
-    this.setState({ address, isAddressValid: false });
-  }
-
-  private onChangeCode = (codeHash: string): void => {
-    const code = store.getCode(codeHash);
-
-    this.setState({ codeHash, isHashValid: !!code });
-
-    if (code) {
-      if (code.contractAbi) {
-        this.onAddAbi(code.json.abi, code.contractAbi, true);
-      } else {
-        this.onAddAbi(null, null, false);
-      }
-
-      this.onChangeName(`${code.json.name} (instance)`);
-    }
-  }
-
-  private onChangeEndowment = (endowment?: BN | null): void => {
-    this.setState({ endowment: endowment || new BN(0) });
-  }
-
-  private onChangeGas = (gasLimit: BN | undefined): void => {
-    this.setState({ gasLimit: gasLimit || new BN(0) });
-  }
-
-  private onChangeName = (name: string): void => {
-    this.setState({ name, isNameValid: name.length !== 0 });
-  }
-
-  private onChangeParams = (params: Array<any>): void => {
-    this.setState({ params });
-  }
-
-  private onValidateAddr = (isAddressValid: boolean): void => {
-    this.setState({ isAddressValid });
-  }
-
-  private toggleBusy = (): void => {
-    this.setState(({ isBusy }) => ({
-      isBusy: !isBusy
-    }));
-  }
-
-  private toggleNew = (): void => {
-    this.setState(({ isNew }) => ({
-      address: null,
-      abi: null,
-      isAddressValid: false,
-      isAbiValid: false,
-      isNameValid: false,
-      isNew: !isNew,
-      name: ''
-    }));
-  }
-
-  private onSave = (): void => {
-    const { address, abi, name } = this.state;
-
-    if (!address || !abi || !name) {
-      return;
-    }
-
-    store.saveContract(new AccountId(address), { abi, name }).catch((error) => {
-      console.error('Unable to save contract', error);
-    });
-
-    this.redirect();
-  }
-
-  private onSuccess = (result: SubmittableResult): void => {
-    const record = result.findRecord('contract', 'Instantiated');
-
-    if (record) {
-      const address = record.event.data[1];
-
-      this.setState(({ abi, name }) => {
-        if (!abi || !name) {
-          return;
-        }
-
-        store.saveContract(address as AccountId, { abi, name }).catch((error) => {
-          console.error('Unable to save contract', error);
-        });
-
-        this.redirect();
-      });
-    }
-
-    this.toggleBusy();
-  }
-
-  private redirect () {
-    window.location.hash = this.props.basePath;
-  }
-}
-
-export default translate(Create);

+ 240 - 0
packages/app-contracts/src/Modal.tsx

@@ -0,0 +1,240 @@
+// Copyright 2017-2019 @polkadot/app-contracts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+
+import BN from 'bn.js';
+import React from 'react';
+import { Button, Input, InputAddress, InputNumber, InputTags, Modal, TxComponent } from '@polkadot/ui-app';
+import { ContractAbi } from '@polkadot/types';
+
+import ABI from './ABI';
+
+export type ContractModalProps = I18nProps & {
+  basePath: string,
+  isNew?: boolean,
+  isOpen: boolean,
+  onClose?: () => void
+};
+
+export type ContractModalState = {
+  abi?: string | null,
+  accountId?: string | null,
+  contractAbi?: ContractAbi | null,
+  gasLimit: BN,
+  isAbiSupplied: boolean,
+  isAbiValid: boolean,
+  isBusy: boolean,
+  isNameValid: boolean,
+  name?: string | null,
+  tags: Array<string>
+};
+
+class ContractModal<P extends ContractModalProps, S extends ContractModalState> extends TxComponent<P, S> {
+  protected defaultState: S = {
+    accountId: null,
+    gasLimit: new BN(0),
+    isAbiSupplied: false,
+    isAbiValid: false,
+    isBusy: false,
+    isNameValid: false,
+    name: null,
+    tags: [] as Array<string>
+  } as S;
+
+  state: S = this.defaultState;
+
+  isContract?: boolean;
+
+  componentWillReceiveProps ({ isOpen }: P, _: S) {
+    if (isOpen && !this.props.isOpen && !this.state.isBusy) {
+      this.reset();
+    }
+  }
+
+  render () {
+    const { isOpen, t } = this.props;
+
+    return (
+      <Modal
+        className='app--contracts-Modal'
+        dimmer='inverted'
+        onClose={this.onClose}
+        open={isOpen}
+      >
+        <Modal.Header>
+          {t(this.headerText)}
+        </Modal.Header>
+        <Modal.Content>
+          {this.renderContent()}
+        </Modal.Content>
+        <Modal.Actions>
+          {this.renderButtons()}
+        </Modal.Actions>
+      </Modal>
+    );
+  }
+
+  protected headerText: string = '';
+  protected renderContent: () => React.ReactNode | null = () => null;
+  protected renderButtons: () => React.ReactNode | null = () => null;
+
+  protected renderInputAbi () {
+    const { t } = this.props;
+    const { isBusy } = this.state;
+
+    return (
+      <ABI
+        help={t(
+          this.isContract ?
+            'The ABI for the WASM code. Since we will be making a call into the code, the ABI is required and stored for future operations such as sending messages.' :
+            'The ABI for the WASM code. In this step it is optional, but setting it here simplifies the setup of contract instances.'
+        )}
+        label={t(
+          this.isContract ?
+            'contract ABI' :
+            'contract ABI (optional)'
+        )}
+        onChange={this.onAddAbi}
+        isDisabled={isBusy}
+        isRequired={this.isContract}
+      />
+    );
+  }
+
+  protected renderInputAccount () {
+    const { t } = this.props;
+    const { accountId, isBusy } = this.state;
+
+    return (
+      <InputAddress
+        defaultValue={accountId}
+        help={t('Specify the user account to use for this deployment. And fees will be deducted from this account.')}
+        isDisabled={isBusy}
+        isInput={false}
+        label={t('deployment account')}
+        onChange={this.onChangeAccount}
+        type='account'
+        value={accountId}
+      />
+    );
+  }
+
+  protected renderInputName () {
+    const { isNew, t } = this.props;
+    const { isBusy, isNameValid, name } = this.state;
+
+    return (
+      <Input
+        defaultValue={name}
+        help={t(
+          this.isContract ?
+            'A name for the deployed contract to help users distinguish. Only used for display purposes.' :
+            'A name for this WASM code to help users distinguish. Only used for display purposes.'
+        )}
+        isDisabled={isBusy}
+        isError={!isNameValid}
+        label={t(
+          this.isContract ?
+            'contract name' :
+            'code bundle name'
+        )}
+        onChange={this.onChangeName}
+        onEnter={this[isNew ? 'sendTx' : 'submit']}
+        value={name || ''}
+      />
+    );
+  }
+
+  protected renderInputTags () {
+    const { t } = this.props;
+    const { isBusy, tags } = this.state;
+
+    return (
+      <InputTags
+        help={t(`Additional user-specified tags. Tags can be used for categorization and filtering.`)}
+        isDisabled={isBusy}
+        label={t('user-defined tags')}
+        onChange={this.onChangeTags}
+        value={tags}
+      />
+    );
+  }
+
+  protected renderInputGas () {
+    const { t } = this.props;
+    const { gasLimit, isBusy } = this.state;
+    const isGasValid = !gasLimit.isZero();
+
+    return (
+      <InputNumber
+        defaultValue={gasLimit}
+        help={t('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
+        isDisabled={isBusy}
+        isError={!isGasValid}
+        label={t('maximum gas allowed')}
+        onChange={this.onChangeGas}
+        onEnter={this.sendTx}
+        value={gasLimit || ''}
+      />
+    );
+  }
+
+  protected renderCancelButton () {
+    const { t } = this.props;
+
+    return (
+      <>
+        <Button
+          isNegative
+          onClick={this.onClose}
+          label={t('Cancel')}
+        />
+        <Button.Or />
+      </>
+    );
+  }
+
+  protected reset = () => {
+    this.setState(
+      this.defaultState
+    );
+  }
+
+  protected toggleBusy = (isBusy?: boolean) => () => {
+    this.setState((state: S) => {
+      return {
+        isBusy: isBusy === undefined ? !state.isBusy : isBusy
+      };
+    });
+  }
+
+  protected onClose = () => {
+    const { onClose } = this.props;
+
+    onClose && onClose();
+  }
+
+  protected onAddAbi = (abi: string | null | undefined, contractAbi: ContractAbi | null = null, isAbiSupplied: boolean = false): void => {
+    this.setState({ abi, contractAbi, isAbiSupplied, isAbiValid: !!abi });
+  }
+
+  protected onChangeAccount = (accountId: string | null): void => {
+    this.setState({ accountId });
+  }
+
+  protected onChangeName = (name: string): void => {
+    this.setState({ name, isNameValid: name.length !== 0 });
+  }
+
+  protected onChangeTags = (tags: Array<string>): void => {
+    this.setState({ tags });
+  }
+
+  protected onChangeGas = (gasLimit: BN | undefined): void => {
+    this.setState({ gasLimit: gasLimit || new BN(0) });
+  }
+}
+
+export default ContractModal;

+ 3 - 1
packages/app-contracts/src/Params.tsx

@@ -10,6 +10,7 @@ import UIParams from '@polkadot/ui-params';
 import { getTypeDef, TypeDef } from '@polkadot/types';
 
 type Props = {
+  isDisabled?: boolean,
   params?: ContractABIArgs,
   onChange: (values: Array<any>) => void,
   onEnter?: () => void
@@ -36,7 +37,7 @@ export default class Params extends React.PureComponent<Props, State> {
   }
 
   render () {
-    const { onEnter } = this.props;
+    const { isDisabled, onEnter } = this.props;
     const { params } = this.state;
 
     if (!params.length) {
@@ -45,6 +46,7 @@ export default class Params extends React.PureComponent<Props, State> {
 
     return (
       <UIParams
+        isDisabled={isDisabled}
         onChange={this.onChange}
         onEnter={onEnter}
         params={params}

+ 94 - 0
packages/app-contracts/src/RemoveABI.tsx

@@ -0,0 +1,94 @@
+// Copyright 2017-2019 @polkadot/app-accounts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { CodeStored } from '@polkadot/app-contracts/types';
+
+import React from 'react';
+import { Button, CodeRow, Modal } from '@polkadot/ui-app';
+
+import translate from './translate';
+
+type Props = I18nProps & {
+  code: CodeStored,
+  onClose: () => void,
+  onRemove: () => void
+};
+
+class RemoveABI extends React.PureComponent<Props> {
+  render () {
+    const { onClose, t } = this.props;
+    return (
+      <Modal
+        className='app--accounts-Modal'
+        dimmer='inverted'
+        onClose={onClose}
+        open
+      >
+        <Modal.Header>
+          {t('Confirm ABI removal')}
+        </Modal.Header>
+        <Modal.Content>
+          {this.renderContent()}
+        </Modal.Content>
+        {this.renderButtons()}
+      </Modal>
+    );
+  }
+
+  private content = () => {
+    const { t } = this.props;
+
+    return (
+      <>
+        <p>{t('You are about to remove this code\'s ABI. Once completed, should you need to access it again, you will have to manually re-upload it.')}</p>
+        <p>{t('This operaion does not impact the associated on-chain code or any of its contracts.')}</p>
+      </>
+    );
+  }
+
+  private renderButtons () {
+    const { onClose, t } = this.props;
+
+    return (
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            isNegative
+            onClick={onClose}
+            label={t('Cancel')}
+          />
+          <Button.Or />
+          <Button
+            isPrimary
+            onClick={this.onRemove}
+            label={t('Remove')}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    );
+  }
+
+  private renderContent () {
+    const { code } = this.props;
+
+    return (
+      <CodeRow
+        code={code}
+        isInline
+      >
+        {this.content()}
+      </CodeRow>
+    );
+  }
+
+  private onRemove = () => {
+    const { onClose, onRemove } = this.props;
+
+    onClose && onClose();
+    onRemove();
+  }
+}
+
+export default translate(RemoveABI);

+ 79 - 54
packages/app-contracts/src/index.tsx

@@ -3,71 +3,66 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { AppProps, I18nProps } from '@polkadot/ui-app/types';
-import { TabItem } from '@polkadot/ui-app/Tabs';
 import { ComponentProps, LocationProps } from './types';
 
 import React from 'react';
-import { Route, Switch } from 'react-router';
+import { Route, RouteComponentProps, Switch } from 'react-router';
+import { withRouter } from 'react-router-dom';
 import { HelpOverlay, Tabs } from '@polkadot/ui-app';
+import { withMulti, withObservable } from '@polkadot/ui-api';
+import keyring from '@polkadot/ui-keyring';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
 
 import introMd from './md/intro.md';
 import store from './store';
 import translate from './translate';
-import Call from './Call';
-import Code from './Code';
-import Instantiate from './Instantiate';
+import Contracts from './Contracts';
+import Codes from './Codes';
+import Deploy from './Deploy';
+
+type Props = AppProps & I18nProps & RouteComponentProps & {
+  accounts: SubjectInfo[],
+  contracts: SubjectInfo[]
+};
 
-type Props = AppProps & I18nProps;
 type State = {
-  tabs: Array<TabItem>,
+  codeHash?: string,
+  hasContracts: boolean,
+  isDeployOpen: boolean,
   updated: number
 };
 
 class App extends React.PureComponent<Props, State> {
-  state: State;
+  state: State = {
+    hasContracts: false,
+    isDeployOpen: false,
+    updated: 0
+  };
 
   constructor (props: Props) {
     super(props);
 
-    const { t } = props;
-
     store.on('new-code', this.triggerUpdate);
-    store.on('new-contract', this.triggerUpdate);
+    store.on('removed-code', this.triggerUpdate);
 
     // since we have a dep on the async API, we load here
     store.loadAll().catch(() => {
       // noop, handled internally
     });
+  }
 
-    this.state = {
-      tabs: [
-        {
-          name: 'call',
-          text: t('Call')
-        },
-        {
-          name: 'instantiate',
-          text: t('Instance')
-        },
-        {
-          name: 'code',
-          text: t('Code')
-        }
-      ],
-      updated: 0
-    };
+  static getDerivedStateFromProps ({ contracts }: Props): State {
+    const hasContracts = !!contracts && Object.keys(contracts).length >= 1;
+
+    return {
+      hasContracts
+    } as State;
   }
 
   render () {
-    const { basePath } = this.props;
-    const { tabs } = this.state;
-    const hidden = store.hasContracts
-      ? []
-      : ['call'];
-
-    if (!store.hasCode) {
-      hidden.push('instantiate');
-    }
+    const { basePath, t } = this.props;
+    const { codeHash, isDeployOpen } = this.state;
+    const hidden: Array<string> = [];
 
     return (
       <main className='contracts--App'>
@@ -76,46 +71,76 @@ class App extends React.PureComponent<Props, State> {
           <Tabs
             basePath={basePath}
             hidden={hidden}
-            items={tabs}
+            items={[
+              {
+                name: 'code',
+                text: 'Code'
+              },
+              {
+                isRoot: true,
+                name: 'contracts',
+                text: 'Contracts'
+              }
+            ].map(tab => ({ ...tab, text: t(tab.text) }))
+            }
           />
         </header>
         <Switch>
-          <Route path={`${basePath}/instantiate`} render={this.renderComponent(Instantiate)} />
-          <Route path={`${basePath}/code`} render={this.renderComponent(Code)} />
-          <Route
-            render={
-              hidden.includes('call')
-                ? (
-                  hidden.includes('instantiate')
-                    ? this.renderComponent(Code)
-                    : this.renderComponent(Instantiate)
-                )
-                : this.renderComponent(Call)
-            }
-          />
+          <Route path={`${basePath}/code`} render={this.renderComponent(Codes)} />
+          <Route render={this.renderComponent(Contracts)} exact />
         </Switch>
+        <Deploy
+          basePath={basePath}
+          codeHash={codeHash}
+          isOpen={isDeployOpen}
+          onClose={this.hideDeploy}
+        />
       </main>
     );
   }
 
   private renderComponent (Component: React.ComponentType<ComponentProps>) {
     return ({ match }: LocationProps) => {
-      const { basePath, location, onStatusChange } = this.props;
+      const { accounts, basePath, contracts, location, onStatusChange } = this.props;
+
+      if (!contracts) {
+        return null;
+      }
 
       return (
         <Component
+          accounts={accounts}
           basePath={basePath}
+          contracts={contracts}
+          hasCode={store.hasCode}
           location={location}
           match={match}
           onStatusChange={onStatusChange}
+          showDeploy={this.showDeploy}
         />
       );
     };
   }
 
+  private showDeploy = (codeHash?: string) => () => {
+    this.setState({
+      codeHash: codeHash || undefined,
+      isDeployOpen: true
+    });
+  }
+
+  private hideDeploy = () => {
+    this.setState({ isDeployOpen: false });
+  }
+
   private triggerUpdate = (): void => {
     this.setState({ updated: Date.now() });
   }
 }
-
-export default translate(App);
+export default withMulti(
+  App,
+  translate,
+  withObservable(keyring.accounts.subject, { propName: 'accounts' }),
+  withObservable(keyring.contracts.subject, { propName: 'contracts' }),
+  withRouter
+);

+ 19 - 47
packages/app-contracts/src/store.ts

@@ -2,57 +2,43 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { CodeJson, ContractJson } from './types';
+import { CodeJson, CodeStored } from './types';
 
 import EventEmitter from 'eventemitter3';
 import store from 'store';
-import { AccountId, ContractAbi, Hash } from '@polkadot/types';
+import { ContractAbi, Hash } from '@polkadot/types';
 import { api } from '@polkadot/ui-api';
 
-const PREFIX = 'contract:';
-const KEY_CODE = `${PREFIX}code:`;
-const KEY_CONTRACT = `${PREFIX}addr:`;
+const KEY_CODE = 'code:';
 
 const codeRegex = new RegExp(`^${KEY_CODE}`, '');
-const contractRegex = new RegExp(`^${KEY_CONTRACT}`, '');
-
-type CodeStored = { json: CodeJson , contractAbi?: ContractAbi };
-type ContractStored = { json: ContractJson , contractAbi: ContractAbi };
 
 class Store extends EventEmitter {
   private allCode: { [index: string]: CodeStored } = {};
-  private allContracts: { [index: string]: ContractStored } = {};
 
   get hasCode (): boolean {
     return Object.keys(this.allCode).length !== 0;
   }
 
-  get hasContracts (): boolean {
-    return Object.keys(this.allContracts).length !== 0;
-  }
-
   getAllCode (): Array<CodeStored> {
     return Object.values(this.allCode);
   }
 
-  getAllContracts (): Array<ContractStored> {
-    return Object.values(this.allContracts);
-  }
-
   getCode (codeHash: string): CodeStored {
     return this.allCode[codeHash];
   }
 
-  getContract (address: string): ContractStored {
-    return this.allContracts[address];
-  }
-
-  async saveCode (codeHash: Hash, partial: Partial<CodeJson>) {
+  async saveCode (codeHash: string | Hash, partial: Partial<CodeJson>) {
     await api.isReady;
 
+    const hex = (typeof codeHash === 'string' ? new Hash(codeHash) : codeHash).toHex();
+
+    const existing = this.getCode(hex);
+
     const json = {
+      ...(existing ? existing.json : {}),
       ...partial,
-      codeHash: codeHash.toHex(),
+      codeHash: hex,
       genesisHash: api.genesisHash.toHex()
     } as CodeJson;
 
@@ -61,18 +47,10 @@ class Store extends EventEmitter {
     this.addCode(json);
   }
 
-  async saveContract (address: AccountId, partial: Partial<ContractJson>) {
-    await api.isReady;
-
-    const json = {
-      ...partial,
-      address: address.toString(),
-      genesisHash: api.genesisHash.toHex()
-    } as ContractJson;
+  forgetCode (codeHash: string) {
+    store.remove(`${KEY_CODE}${codeHash}`);
 
-    store.set(`${KEY_CONTRACT}${address}`, json);
-
-    this.addContract(json);
+    this.removeCode(codeHash);
   }
 
   async loadAll () {
@@ -81,19 +59,17 @@ class Store extends EventEmitter {
 
       const genesisHash = api.genesisHash.toHex();
 
-      store.each((json: CodeJson | ContractJson, key: string) => {
+      store.each((json: CodeJson, key: string) => {
         if (json && json.genesisHash !== genesisHash) {
           return;
         }
 
         if (codeRegex.test(key)) {
-          this.addCode(json as CodeJson);
-        } else if (contractRegex.test(key)) {
-          this.addContract(json as ContractJson);
+          this.addCode(json);
         }
       });
     } catch (error) {
-      console.error('Unable to load contracts', error);
+      console.error('Unable to load code', error);
     }
   }
 
@@ -112,14 +88,10 @@ class Store extends EventEmitter {
     }
   }
 
-  private addContract (json: ContractJson) {
+  private removeCode (codeHash: string) {
     try {
-      this.allContracts[json.address] = {
-        json,
-        contractAbi: new ContractAbi(JSON.parse(json.abi))
-      };
-
-      this.emit('new-contract');
+      delete this.allCode[codeHash];
+      this.emit('removed-code');
     } catch (error) {
       console.error(error);
     }

+ 17 - 8
packages/app-contracts/src/types.ts

@@ -3,6 +3,8 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { AppProps } from '@polkadot/ui-app/types';
+import { ContractAbi } from '@polkadot/types';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
 
 export type LocationProps = {
   match: {
@@ -10,19 +12,26 @@ export type LocationProps = {
   }
 };
 
-export type ComponentProps = AppProps & LocationProps;
-
-type BaseInfo = {
-  name: string,
-  genesisHash: string
+export type ComponentProps = AppProps & LocationProps & {
+  accounts: SubjectInfo[],
+  contracts: SubjectInfo[],
+  hasCode: boolean,
+  showDeploy: (codeHash?: string) => () => void
 };
 
-export type CodeJson = BaseInfo & {
+export type CodeJson = {
   abi?: string | null,
   codeHash: string
+  name: string,
+  genesisHash: string,
+  tags: Array<string>
 };
 
-export type ContractJson = BaseInfo & {
+export type CodeStored = { json: CodeJson , contractAbi?: ContractAbi };
+
+export type ContractJsonOld = {
+  genesisHash: string,
   abi: string,
-  address: string
+  address: string,
+  name: string
 };

+ 1 - 0
packages/app-council/src/Overview/Candidate.tsx

@@ -16,6 +16,7 @@ export default class Candidate extends React.PureComponent<Props> {
     return (
       <AddressCard
         defaultName='candidate'
+        type='address'
         value={address}
       />
     );

+ 1 - 0
packages/app-council/src/Overview/Member.tsx

@@ -24,6 +24,7 @@ class Member extends React.PureComponent<Props> {
       <AddressCard
         buttons={<div><label>{t('active until')}</label>#{formatNumber(block)}</div>}
         defaultName='council member'
+        type='address'
         value={address}
       />
     );

+ 1 - 0
packages/app-democracy/src/index.tsx

@@ -32,6 +32,7 @@ class App extends React.PureComponent<Props, State> {
     this.state = {
       tabs: [
         {
+          isRoot: true,
           name: 'overview',
           text: t('Democracy overview')
         },

+ 1 - 0
packages/app-explorer/src/index.tsx

@@ -48,6 +48,7 @@ class ExplorerApp extends React.Component<Props, State> {
     this.state = {
       items: [
         {
+          isRoot: true,
           name: 'chain',
           text: t('Chain info')
         },

+ 1 - 0
packages/app-extrinsics/src/index.tsx

@@ -22,6 +22,7 @@ class ExtrinsicsApp extends React.PureComponent<Props> {
           <Tabs
             basePath={basePath}
             items={[{
+              isRoot: true,
               name: 'create',
               text: t('Extrinsic submission')
             }]}

+ 1 - 0
packages/app-settings/src/index.tsx

@@ -30,6 +30,7 @@ class App extends React.PureComponent<Props, State> {
     this.state = {
       tabs: [
         {
+          isRoot: true,
           name: 'general',
           text: t('General')
         },

+ 1 - 0
packages/app-staking/src/Account/index.tsx

@@ -109,6 +109,7 @@ class Account extends React.PureComponent<Props, State> {
     return (
       <AddressCard
         buttons={this.renderButtons()}
+        type='account'
         value={accountId}
       >
         {this.renderBond()}

+ 2 - 2
packages/app-staking/src/Accounts.tsx

@@ -7,7 +7,7 @@ import { AccountFilter, ComponentProps } from './types';
 
 import React from 'react';
 import { CardGrid, Dropdown, FilterOverlay } from '@polkadot/ui-app';
-import { getAddrName } from '@polkadot/ui-app/util';
+import { getAddressName } from '@polkadot/ui-app/util';
 import keyring from '@polkadot/ui-keyring';
 import createOption from '@polkadot/ui-keyring/options/item';
 
@@ -81,7 +81,7 @@ class Accounts extends React.PureComponent<Props, State> {
     const { stashes } = this.props;
 
     return stashes.map((stashId) =>
-      createOption(stashId, getAddrName(stashId))
+      createOption(stashId, getAddressName(stashId, 'account'))
     );
   }
 

+ 1 - 0
packages/app-staking/src/Overview/Address.tsx

@@ -98,6 +98,7 @@ class Address extends React.PureComponent<Props, State> {
         defaultName={defaultName}
         iconInfo={this.renderOffline()}
         key={stashId || controllerId}
+        type='address'
         value={stashId}
         withBalance={{ bonded }}
       >

+ 1 - 0
packages/app-staking/src/index.tsx

@@ -53,6 +53,7 @@ class App extends React.PureComponent<Props, State> {
       stashes: [],
       tabs: [
         {
+          isRoot: true,
           name: 'overview',
           text: t('Staking overview')
         },

+ 1 - 0
packages/app-storage/src/Selection/index.tsx

@@ -34,6 +34,7 @@ class Selection extends React.PureComponent<Props, State> {
     this.state = {
       items: [
         {
+          isRoot: true,
           name: 'modules',
           text: t('Modules')
         },

+ 1 - 0
packages/app-sudo/src/index.tsx

@@ -49,6 +49,7 @@ class App extends React.PureComponent<Props, State> {
             basePath={basePath}
             items={[
               {
+                isRoot: true,
                 name: 'index',
                 text: t('Sudo access')
               },

+ 1 - 0
packages/app-toolbox/src/index.tsx

@@ -34,6 +34,7 @@ class ToolboxApp extends React.PureComponent<Props, State> {
     this.state = {
       tabs: [
         {
+          isRoot: true,
           name: 'rpc',
           text: t('RPC calls')
         },

+ 1 - 1
packages/ui-api/package.json

@@ -31,7 +31,7 @@
   "homepage": "https://github.com/polkadot-js/ui/tree/master/packages/ui-reactive#readme",
   "dependencies": {
     "@babel/runtime": "^7.4.5",
-    "@polkadot/api": "^0.81.0-beta.3",
+    "@polkadot/api": "^0.81.0-beta.7",
     "@polkadot/extension-dapp": "^0.1.1-beta.25",
     "rxjs-compat": "^6.4.0"
   }

+ 1 - 0
packages/ui-api/src/Api.tsx

@@ -128,6 +128,7 @@ export default class Api extends React.PureComponent<Props, State> {
     // finally load the keyring
     keyring.loadAll({
       addressPrefix: properties.get('networkId'),
+      genesisHash: api.genesisHash,
       isDevelopment,
       type: 'ed25519'
     }, injectedAccounts);

+ 6 - 3
packages/ui-app/src/AddressCard.tsx

@@ -1,15 +1,18 @@
 // Copyright 2017-2019 @polkadot/ui-app authors & contributors
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
+// import { I18nProps } from '@polkadot/ui-app/types';
 
 import React from 'react';
 import styled from 'styled-components';
 
-import AddressRow, { RowProps } from './AddressRow';
+import AddressRow, { Props as AddressProps } from './AddressRow';
 import Card from './Card';
 import LinkPolkascan from './LinkPolkascan';
 
-type Props = RowProps & {
+import translate from './translate';
+
+type Props = AddressProps & {
   withExplorer?: boolean
 };
 
@@ -37,7 +40,7 @@ class AddressCard extends React.PureComponent<Props> {
   }
 }
 
-export default styled(AddressCard)`
+export default styled(translate(AddressCard))`
   display: flex;
   flex-direction: column;
   justify-content: space-between;

+ 5 - 3
packages/ui-app/src/AddressMini.tsx

@@ -8,8 +8,9 @@ import BN from 'bn.js';
 import React from 'react';
 import styled from 'styled-components';
 import { AccountId, AccountIndex, Address } from '@polkadot/types';
+import { KeyringItemType } from '@polkadot/ui-keyring/types';
 
-import { classes, getAddrName, toShortAddress } from './util';
+import { classes, getAddressName, toShortAddress } from './util';
 import BalanceDisplay from './Balance';
 import BondedDisplay from './Bonded';
 import IdentityIcon from './IdentityIcon';
@@ -21,6 +22,7 @@ type Props = BareProps & {
   iconInfo?: React.ReactNode,
   isPadded?: boolean,
   isShort?: boolean,
+  type?: KeyringItemType,
   value?: AccountId | AccountIndex | Address | string,
   withAddress?: boolean,
   withBalance?: boolean,
@@ -66,13 +68,13 @@ class AddressMini extends React.PureComponent<Props> {
   }
 
   private renderAddressOrName (address: string) {
-    const { isShort = true, withAddress = true } = this.props;
+    const { isShort = true, withAddress = true, type = 'address' } = this.props;
 
     if (!withAddress) {
       return null;
     }
 
-    const name = getAddrName(address);
+    const name = getAddressName(address, type, true);
 
     return (
       <div className={`ui--AddressMini-address ${name ? 'withName' : 'withAddr'}`}>{

+ 34 - 317
packages/ui-app/src/AddressRow.tsx

@@ -2,57 +2,43 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
+import { I18nProps } from '@polkadot/ui-app/types';
 import { AccountId, AccountIndex, Address } from '@polkadot/types';
-import { I18nProps } from './types';
+import { KeyringItemType } from '@polkadot/ui-keyring/types';
 
 import BN from 'bn.js';
-import { Label } from 'semantic-ui-react';
 import React from 'react';
 import styled from 'styled-components';
 import { withCalls, withMulti } from '@polkadot/ui-api';
-import { Button, Input, InputTags } from '@polkadot/ui-app';
 import BaseIdentityIcon from '@polkadot/ui-identicon';
 import keyring from '@polkadot/ui-keyring';
 
 import AddressInfo, { BalanceActiveType } from './AddressInfo';
 import CopyButton from './CopyButton';
 import IdentityIcon from './IdentityIcon';
+import Row, { RowProps, RowState, styles } from './Row';
 import translate from './translate';
-import { classes, getAddrName, getAddrTags, toShortAddress } from './util';
+import { classes, getAddressName, getAddressTags, toShortAddress } from './util';
 
-export type RowProps = {
+export type Props = I18nProps & RowProps & {
+  accounts_idAndIndex?: [AccountId?, AccountIndex?]
   bonded?: BN | Array<BN>,
-  buttons?: React.ReactNode,
-  children?: React.ReactNode,
-  className?: string,
-  defaultName?: string,
-  extraInfo?: React.ReactNode,
-  iconInfo?: React.ReactNode,
-  isEditable?: boolean,
-  isInline?: boolean,
+  isContract?: boolean,
+  isValid?: boolean,
+  type: KeyringItemType,
   value: AccountId | AccountIndex | Address | string | null,
   withBalance?: boolean | BalanceActiveType,
-  withIcon?: boolean,
-  withIndex?: boolean,
-  withTags?: boolean
-};
-
-type Props = I18nProps & RowProps & {
-  accounts_idAndIndex?: [AccountId?, AccountIndex?]
+  withIndex?: boolean
 };
 
-type State = {
-  address: string,
-  isEditingName: boolean,
-  isEditingTags: boolean,
-  name: string,
-  tags: string[]
+type State = RowState & {
+  address: string
 };
 
 const DEFAULT_ADDR = '5'.padEnd(16, 'x');
 const ICON_SIZE = 48;
 
-class AddressRow extends React.PureComponent<Props, State> {
+class AddressRow extends Row<Props, State> {
   state: State;
 
   constructor (props: Props) {
@@ -61,18 +47,14 @@ class AddressRow extends React.PureComponent<Props, State> {
     this.state = this.createState();
   }
 
-  static defaultProps = {
-    defaultName: '<unknown>'
-  };
-
-  static getDerivedStateFromProps ({ accounts_idAndIndex = [], defaultName, value }: Props, prevState: State) {
+  static getDerivedStateFromProps ({ accounts_idAndIndex = [], defaultName, type, value }: Props, prevState: State) {
     const [_accountId] = accounts_idAndIndex;
     const accountId = _accountId || value;
     const address = accountId
       ? accountId.toString()
       : DEFAULT_ADDR;
-    const name = getAddrName(address, false, defaultName) || '';
-    const tags = getAddrTags(address);
+    const name = getAddressName(address, type, false, defaultName) || '';
+    const tags = getAddressTags(address, type);
     const state = { tags } as State;
     let hasChanged = false;
 
@@ -92,22 +74,22 @@ class AddressRow extends React.PureComponent<Props, State> {
   }
 
   render () {
-    const { accounts_idAndIndex = [], className, isInline, style } = this.props;
+    const { accounts_idAndIndex = [], className, isContract, isInline, style } = this.props;
     const [accountId, accountIndex] = accounts_idAndIndex;
-    const isValid = accountId || accountIndex;
+    const isValid = this.props.isValid || accountId || accountIndex;
 
     return (
       <div
-        className={classes('ui--AddressRow', !isValid && 'invalid', isInline && 'inline', className)}
+        className={classes('ui--Row', !isValid && 'invalid', isInline && 'inline', className)}
         style={style}
       >
-        <div className='ui--AddressRow-base'>
+        <div className='ui--Row-base'>
           {this.renderIcon()}
-          <div className='ui--AddressRow-details'>
+          <div className='ui--Row-details'>
             {this.renderName()}
             {this.renderAddress()}
             {this.renderAccountIndex()}
-            {this.renderBalances()}
+            {!isContract && this.renderBalances()}
             {this.renderTags()}
           </div>
           {this.renderButtons()}
@@ -118,37 +100,28 @@ class AddressRow extends React.PureComponent<Props, State> {
   }
 
   private createState () {
-    const { accounts_idAndIndex = [], defaultName, value } = this.props;
+    const { accounts_idAndIndex = [], defaultName, type, value } = this.props;
     const [_accountId] = accounts_idAndIndex;
     const accountId = _accountId || value;
     const address = accountId
       ? accountId.toString()
       : DEFAULT_ADDR;
-    const name = getAddrName(address, false, defaultName) || '';
-    const tags = getAddrTags(address);
+    const name = getAddressName(address, type, false, defaultName) || '';
+    const tags = getAddressTags(address, type);
 
     return {
+      ...this.state,
       address,
-      isEditingName: false,
-      isEditingTags: false,
       name,
       tags
     };
   }
 
-  private onChangeName = (name: string) => {
-    this.setState({ name });
-  }
-
-  private onChangeTags = (tags: string[]) => {
-    this.setState({ tags });
-  }
-
   private renderAddress () {
     const { address } = this.state;
 
     return (
-      <div className='ui--AddressRow-accountId'>
+      <div className='ui--Row-accountId'>
         <CopyButton
           isAddress
           value={address}
@@ -168,7 +141,7 @@ class AddressRow extends React.PureComponent<Props, State> {
     }
 
     return (
-      <div className='ui--AddressRow-accountIndex'>
+      <div className='ui--Row-accountIndex'>
         {accountIndex.toString()}
       </div>
     );
@@ -183,7 +156,7 @@ class AddressRow extends React.PureComponent<Props, State> {
     }
 
     return (
-      <div className='ui--AddressRow-balances'>
+      <div className='ui--Row-balances'>
         <AddressInfo
           value={accountId}
           withBalance={withBalance}
@@ -192,46 +165,6 @@ class AddressRow extends React.PureComponent<Props, State> {
     );
   }
 
-  private renderButtons () {
-    const { buttons } = this.props;
-
-    return buttons
-      ? <div className='ui--AddressRow-buttons'>{buttons}</div>
-      : null;
-  }
-
-  private renderChildren () {
-    const { children } = this.props;
-    // we need children, or when an array, at least 1 non-empty value
-    const hasChildren = !children
-      ? false
-      : Array.isArray(children)
-        ? children.filter((child) => child).length !== 0
-        : true;
-
-    if (!hasChildren) {
-      return null;
-    }
-
-    return (
-      <div className='ui--AddressRow-children'>
-        {children}
-      </div>
-    );
-  }
-
-  private renderEditIcon () {
-    return (
-      <Button
-        className='iconButton'
-        icon='edit'
-        size='mini'
-        isPrimary
-        key='unlock'
-      />
-    );
-  }
-
   private renderIcon () {
     const { accounts_idAndIndex = [], iconInfo, withIcon = true } = this.props;
     const { address } = this.state;
@@ -248,13 +181,13 @@ class AddressRow extends React.PureComponent<Props, State> {
       : BaseIdentityIcon;
 
     return (
-      <div className='ui--AddressRow-icon'>
+      <div className='ui--Row-icon'>
         <Component
           size={ICON_SIZE}
           value={address}
         />
         {iconInfo && (
-          <div className='ui--AddressRow-icon-info'>
+          <div className='ui--Row-icon-info'>
             {iconInfo}
           </div>
         )}
@@ -262,73 +195,7 @@ class AddressRow extends React.PureComponent<Props, State> {
     );
   }
 
-  private renderName () {
-    const { isEditable } = this.props;
-    const { isEditingName, name } = this.state;
-
-    return isEditingName
-      ? (
-        <Input
-          autoFocus
-          className='ui--AddressRow-name-input'
-          defaultValue={name}
-          onBlur={this.saveName}
-          onChange={this.onChangeName}
-          onEnter={this.saveName}
-          withLabel={false}
-        />
-      )
-      : (
-        <div
-          className={classes('ui--AddressRow-name', isEditable && 'editable')}
-          onClick={isEditable ? this.toggleNameEditor : undefined}
-        >
-          {name}
-          {isEditable && this.renderEditIcon()}
-        </div>
-      );
-  }
-
-  private renderTags () {
-    const { isEditingTags, tags } = this.state;
-    const { isEditable, withTags = false } = this.props;
-
-    if (!withTags) {
-      return null;
-    }
-
-    return isEditingTags
-      ? (
-        <InputTags
-          className='ui--AddressRow-tags-input'
-          onBlur={this.saveTags}
-          onChange={this.onChangeTags}
-          onClose={this.saveTags}
-          openOnFocus
-          defaultValue = {tags}
-          searchInput={{ autoFocus: true }}
-          value={tags}
-          withLabel={false}
-        />
-      )
-      : (
-        <div
-          className={classes('ui--AddressRow-tags', isEditable && 'editable')}
-          onClick={isEditable ? this.toggleTagsEditor : undefined}
-        >
-          {
-            !tags.length
-              ? (isEditable ? <span className='addTags'>add tags</span> : undefined)
-              : tags.map((tag) => (
-                <Label key={tag} size='tiny' color='grey'>{tag}</Label>
-              ))
-          }
-          {isEditable && this.renderEditIcon()}
-        </div>
-      );
-  }
-
-  private saveName = () => {
+  protected saveName = () => {
     const { address, name } = this.state;
     const trimmedName = name.trim();
     const meta = {
@@ -350,7 +217,7 @@ class AddressRow extends React.PureComponent<Props, State> {
     }
   }
 
-  private saveTags = () => {
+  protected saveTags = () => {
     const { address, tags } = this.state;
     const meta = {
       tags,
@@ -369,18 +236,6 @@ class AddressRow extends React.PureComponent<Props, State> {
       this.setState({ isEditingTags: false });
     }
   }
-
-  private toggleNameEditor = () => {
-    this.setState(({ isEditingName }) => ({
-      isEditingName: !isEditingName
-    }));
-  }
-
-  private toggleTagsEditor = () => {
-    this.setState(({ isEditingTags }) => ({
-      isEditingTags: !isEditingTags
-    }));
-  }
 }
 
 export {
@@ -390,145 +245,7 @@ export {
 
 export default withMulti(
   styled(AddressRow as React.ComponentClass<Props>)`
-    text-align: left;
-
-    &.inline {
-      display: flex;
-
-      .ui--AddressRow-children {
-        padding: 0 0 0 3rem;
-      }
-    }
-
-    &.invalid {
-      filter: grayscale(100);
-      opacity: 0.5;
-    }
-
-    button.ui.icon.editButton {
-      padding: 0em .3em .3em .3em;
-      color: #2e86ab;
-      background: none;
-      /*trick to let the button in the flow but keep the content centered regardless*/
-      margin-left: -2em;
-      position: relative;
-      right: -2.3em;
-      z-index: 1;
-    }
-
-    .ui--AddressRow-accountId,
-    .ui--AddressRow-accountIndex {
-      font-family: monospace;
-      font-size: 1.25em;
-      padding: 0;
-      margin-bottom: 0.25rem;
-      white-space: nowrap;
-    }
-
-    .ui--AddressRow-accountIndex {
-      font-style: italic;
-    }
-
-    .ui--AddressRow-balances {
-      .column {
-        display: block;
-
-        label,
-        .result {
-          display: inline-block;
-        }
-      }
-
-      > span {
-        text-align: left;
-      }
-    }
-
-    .ui--AddressRow-base {
-      display: flex;
-    }
-
-    .ui--AddressRow-buttons {
-      flex: 0;
-      margin: -0.75rem -1rem 0 0;
-      white-space: nowrap;
-
-      button.ui.button:last-child {
-        margin-right: 0;
-      }
-    }
-
-    .ui--AddressRow-children {
-      display: block;
-      padding-top: 1rem;
-    }
-
-    .ui--AddressRow-details {
-      flex: 1;
-      margin-right: 1rem;
-      padding: 0.25rem 0 0;
-
-      * {
-        vertical-align: middle;
-      }
-    }
-
-    .ui--AddressRow-icon {
-      flex: 0;
-      margin-right: 1em;
-      position: relative;
-
-      .ui--AddressRow-icon-info {
-        left: -0.5rem;
-        position: absolute;
-        top: -0.5rem;
-      }
-    }
-
-    .ui--AddressRow-name {
-      box-sizing: border-box;
-      margin: 0;
-      padding: 0;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      text-transform: uppercase;
-      white-space: normal;
-    }
-
-    .ui--AddressRow-name-input {
-      input {
-        height: 1em;
-        text-transform: uppercase;
-        margin-top: -0.3em;
-        margin-bottom: -0.35em;
-      }
-    }
-
-    .ui--AddressRow-tags {
-      &.editable {
-        display: flex;
-        flex-wrap: wrap;
-        justify-content: left;
-
-        > span {
-          border: 1px #00000052 solid;
-          border-radius: .5em;
-          border-style: dashed;
-          color: grey;
-          font-size: x-small;
-          padding: .1em 0.3em 0.1em 0.3em;
-          margin-top: .2em;
-        }
-
-        > div.label {
-          margin-top:.3em
-        }
-      }
-    }
-
-    .ui--AddressRow-tags-input {
-      margin-bottom: -1.4em;
-    }
+    ${styles}
   `,
   translate,
   withCalls<Props>(

+ 15 - 2
packages/ui-app/src/Button/Button.tsx

@@ -8,6 +8,7 @@ import React from 'react';
 import SUIButton from 'semantic-ui-react/dist/commonjs/elements/Button/Button';
 import { isUndefined } from '@polkadot/util';
 
+import Icon from '../Icon';
 import Tooltip from '../Tooltip';
 
 let idCounter = 0;
@@ -16,7 +17,7 @@ export default class Button extends React.PureComponent<ButtonProps> {
   private id: string = `button-${++idCounter}`;
 
   render () {
-    const { children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isLoading = false, isNegative = false, isPositive = false, isPrimary = false, label, onClick, size, style, tabIndex, tooltip } = this.props;
+    const { children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isLoading = false, isNegative = false, isPositive = false, isPrimary = false, label, labelIcon, labelPosition, onClick, size, style, tabIndex, tooltip } = this.props;
 
     const props = {
       basic: isBasic,
@@ -27,6 +28,7 @@ export default class Button extends React.PureComponent<ButtonProps> {
       disabled: isDisabled,
       floated,
       icon,
+      labelPosition,
       loading: isLoading,
       negative: isNegative,
       onClick,
@@ -43,7 +45,18 @@ export default class Button extends React.PureComponent<ButtonProps> {
         {
           isUndefined(label) && isUndefined(children)
             ? <SUIButton {...props} />
-            : <SUIButton {...props}>{label}{children}</SUIButton>
+            : (
+              <SUIButton {...props}>
+                {!!labelIcon && (
+                  <>
+                    <Icon className={labelIcon} />
+                    {'  '}
+                  </>
+                )}
+                {label}
+                {children}
+              </SUIButton>
+            )
         }
         {tooltip && (
           <Tooltip

+ 2 - 0
packages/ui-app/src/Button/types.ts

@@ -18,6 +18,8 @@ export type ButtonProps = BareProps & {
   isPositive?: boolean,
   isPrimary?: boolean,
   label?: React.ReactNode,
+  labelIcon?: string,
+  labelPosition?: 'left' | 'right',
   onClick?: () => void | Promise<void>,
   ref?: any,
   size?: Button$Sizes,

+ 6 - 0
packages/ui-app/src/Card.tsx

@@ -24,9 +24,14 @@ class Card extends React.PureComponent<Props> {
 
 export default styled(Card)`
   position: relative;
+  flex: 1 1;
+  min-width: 24%;
+  min-height: 130px;
+  justify-content: space-around;
 
   i.help.circle.icon,
   .ui.button.mini,
+  .ui.button.tiny,
   .addTags {
     visibility: hidden;
   }
@@ -34,6 +39,7 @@ export default styled(Card)`
   &:hover {
     i.help.circle.icon,
     .ui.button.mini,
+    .ui.button.tiny,
     .addTags {
       visibility: visible;
     }

+ 59 - 16
packages/ui-app/src/CardGrid.tsx

@@ -2,19 +2,28 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
+import { I18nProps } from '@polkadot/ui-app/types';
+
 import React from 'react';
 import styled from 'styled-components';
 
-type Props = {
+import translate from './translate';
+
+type Props = I18nProps & {
   buttons?: React.ReactNode,
   children: React.ReactNode,
-  className?: string
+  className?: string,
+  emptyText?: string
 };
 
 class CardGrid extends React.PureComponent<Props> {
   render () {
     const { buttons, children, className } = this.props;
 
+    if (!children || (children as Array<any>).length <= 0) {
+      return this.empty();
+    }
+
     return (
       <div className={className}>
         {buttons && (
@@ -31,24 +40,58 @@ class CardGrid extends React.PureComponent<Props> {
       </div>
     );
   }
+
+  empty () {
+    const { buttons, className, emptyText, t } = this.props;
+
+    return (
+      <div className={className}>
+        <div className='ui--CardGrid-empty'>
+          <h2>
+            {emptyText || t('No items')}
+          </h2>
+          {buttons && (
+            <div className='ui--CardGrid-buttons'>
+              {buttons}
+            </div>
+          )}
+          <div className='ui--CardGrid-spacer' />
+        </div>
+      </div>
+    );
+  }
 }
 
-export default styled(CardGrid)`
-  .ui--CardGrid-grid {
-    display: flex;
-    flex-wrap: wrap;
+export default translate(
+  styled(CardGrid)`
+    .ui--CardGrid-grid {
+      display: flex;
+      flex-wrap: wrap;
 
-    > .ui--CardGrid-spacer {
+      > .ui--CardGrid-spacer {
+        flex: 1 1;
+        margin: 0.25rem;
+        padding: 1 1.5rem;
+      }
+    }
+
+    .ui--Card,
+    .ui--CardGrid-spacer {
       flex: 1 1;
-      margin: 0.25rem;
-      padding: 0 1.5rem;
+      min-width: 35rem;
+      max-width: 71rem;
     }
-  }
 
-  .ui--Card,
-  .ui--CardGrid-spacer {
-    flex: 1 1;
-    min-width: 35rem;
-    max-width: 71rem;
+    .ui--CardGrid-empty {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      flex-direction: column;
+      margin: 6rem 0;
+
+      > h2 {
+        margin-bottom: 2rem;
+      }
+    }
   }
-`;
+`);

+ 200 - 0
packages/ui-app/src/CodeRow.tsx

@@ -0,0 +1,200 @@
+// Copyright 2017-2019 @polkadot/ui-app authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { Hash } from '@polkadot/types';
+import { CodeStored } from '@polkadot/app-contracts/types';
+
+import React from 'react';
+import styled from 'styled-components';
+import { withMulti } from '@polkadot/ui-api';
+import { CopyButton, Icon, Messages } from '@polkadot/ui-app';
+import { classes, toShortAddress } from '@polkadot/ui-app/util';
+import contracts from '@polkadot/app-contracts/store';
+
+import Row, { RowProps, RowState, styles } from './Row';
+import translate from './translate';
+
+type Props = I18nProps & RowProps & {
+  code: CodeStored,
+  withMessages?: boolean
+};
+
+type State = RowState & {
+  codeHash: string
+};
+
+const DEFAULT_HASH = '0x';
+const DEFAULT_NAME = '<unknown>';
+
+const CodeIcon = styled.div`
+  & {
+    margin-right: 1em;
+    background: #eee;
+    color: #666;
+    width: 4rem;
+    height: 5rem;
+    padding: 0.5rem;
+    display: flex;
+    justify-content: flex-end;
+    align-items: flex-end;
+  }
+`;
+
+class CodeRow extends Row<Props, State> {
+  state: State;
+
+  constructor (props: Props) {
+    super(props);
+
+    this.state = this.createState();
+  }
+
+  static getDerivedStateFromProps ({ code: { json } }: Props, prevState: State): State | null {
+    const codeHash = json.codeHash || DEFAULT_HASH;
+    const name = json.name || DEFAULT_NAME;
+    const tags = json.tags || [];
+
+    const state = { tags } as State;
+    let hasChanged = false;
+
+    if (codeHash !== prevState.codeHash) {
+      state.codeHash = codeHash;
+      hasChanged = true;
+    }
+
+    if (!prevState.isEditingName && name !== prevState.name) {
+      state.name = name;
+      hasChanged = true;
+    }
+
+    return hasChanged
+      ? state
+      : null;
+  }
+
+  render () {
+    const { className, isInline, style } = this.props;
+
+    return (
+      <div
+        className={classes('ui--Row', isInline && 'inline', className)}
+        style={style}
+      >
+        <div className='ui--Row-base'>
+          {this.renderIcon()}
+          <div className='ui--Row-details'>
+            {this.renderName()}
+            {this.renderCodeHash()}
+            {this.renderTags()}
+          </div>
+          {this.renderButtons()}
+        </div>
+        {this.renderMessages()}
+        {this.renderChildren()}
+      </div>
+    );
+  }
+
+  private createState () {
+    const { code: { json: { codeHash = DEFAULT_HASH, name = DEFAULT_NAME, tags = [] } } } = this.props;
+
+    return {
+      codeHash,
+      isEditingName: false,
+      isEditingTags: false,
+      name,
+      tags
+    };
+  }
+
+  protected renderCodeHash () {
+    const { codeHash } = this.state;
+
+    return (
+      <>
+        <div className='ui--Row-name'>
+          {name}
+        </div>
+        <div className='ui--Row-accountId'>
+          <CopyButton
+            isAddress
+            value={codeHash}
+          >
+            <span>{toShortAddress(codeHash)}</span>
+          </CopyButton>
+        </div>
+      </>
+    );
+  }
+
+  protected renderButtons () {
+    const { buttons } = this.props;
+
+    if (!buttons) {
+      return null;
+    }
+
+    return (
+      <div className='ui--Row-buttons'>
+        {buttons}
+      </div>
+    );
+  }
+
+  protected renderIcon () {
+    return (
+      <CodeIcon>
+        <Icon
+          name='code'
+          size='large'
+        />
+      </CodeIcon>
+    );
+  }
+
+  protected renderMessages () {
+    const { code: { contractAbi }, withMessages } = this.props;
+
+    if (!withMessages || !contractAbi) {
+      return null;
+    }
+
+    return (
+      <Messages
+        contractAbi={contractAbi}
+        isRemovable
+      />
+    );
+  }
+
+  protected saveName = async () => {
+    const { codeHash, name } = this.state;
+    const trimmedName = name.trim();
+
+    // Save only if the name was changed or if it's no empty.
+    if (trimmedName && codeHash) {
+      await contracts.saveCode(new Hash(codeHash), { name });
+
+      this.setState({ isEditingName: false });
+    }
+  }
+
+  protected saveTags = async () => {
+    const { codeHash, tags } = this.state;
+
+    if (codeHash) {
+      await contracts.saveCode(new Hash(codeHash), { tags });
+
+      this.setState({ isEditingTags: false });
+    }
+  }
+}
+
+export default withMulti(
+  styled(CodeRow as React.ComponentClass<Props>)`
+    ${styles}
+  `,
+  translate
+);

+ 141 - 0
packages/ui-app/src/Forget.tsx

@@ -0,0 +1,141 @@
+// Copyright 2017-2019 @polkadot/app-accounts authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { I18nProps } from '@polkadot/ui-app/types';
+import { CodeStored } from '@polkadot/app-contracts/types';
+
+import React from 'react';
+import { AddressRow, Button, CodeRow, Modal } from '@polkadot/ui-app';
+
+import translate from './translate';
+
+type Props = I18nProps & {
+  address?: string,
+  code?: CodeStored,
+  name?: string,
+  mode?: 'account' | 'address' | 'contract' | 'code',
+  onClose: () => void,
+  onForget: () => void
+};
+
+class Forget extends React.PureComponent<Props> {
+  render () {
+    const { onClose } = this.props;
+    return (
+      <Modal
+        className='app--accounts-Modal'
+        dimmer='inverted'
+        onClose={onClose}
+        open
+      >
+        <Modal.Header>
+          {this.headerText()}
+        </Modal.Header>
+        <Modal.Content>
+          {this.renderContent()}
+        </Modal.Content>
+        {this.renderButtons()}
+      </Modal>
+    );
+  }
+
+  private headerText = () => {
+    const { mode = 'account', t } = this.props;
+    switch (mode) {
+      case 'account':
+        return t('Confirm account removal');
+      case 'address':
+        return t('Confirm address removal');
+      case 'contract':
+        return t('Confirm contract removal');
+      case 'code':
+        return t('Confirm code removal');
+    }
+  }
+
+  private content = () => {
+    const { mode = 'account', t } = this.props;
+    switch (mode) {
+      case 'account':
+        return (
+          <>
+            <p>{t('You are about to remove this account from your list of available accounts. Once completed, should you need to access it again, you will have to re-create the account either via seed or via a backup file.')}</p>
+            <p>{t('This operaion does not remove the history of the account from the chain, nor any associated funds from the account. The forget operation only limits your access to the account on this browser.')}</p>
+          </>
+        );
+      case 'address':
+        return (
+          <>
+            <p>{t('You are about to remove this address from your address book. Once completed, should you need to access it again, you will have to re-add the address.')}</p>
+            <p>{t('This operation does not remove the history of the account from the chain, nor any associated funds from the account. The forget operation only limits your access to the address on this browser.')}</p>
+          </>
+        );
+      case 'contract':
+        return (
+          <>
+            <p>{t('You are about to remove this contract from your list of available contracts. Once completed, should you need to access it again, you will have to manually add the contract\'s address in the Instantiate tab.')}</p>
+            <p>{t('This operaion does not remove the history of the contract from the chain, nor any associated funds from its account. The forget operation only limits your access to the contract on this browser.')}</p>
+          </>
+        );
+      case 'code':
+        return (
+          <>
+            <p>{t('You are about to remove this code from your list of available code hashes. Once completed, should you need to access it again, you will have to manually add the code hash again.')}</p>
+            <p>{t('This operaion does not remove the uploaded code WASM and ABI from the chain, nor any deployed contracts. The forget operation only limits your access to the code on this browser.')}</p>
+          </>
+        );
+    }
+  }
+
+  private renderButtons () {
+    const { onClose, onForget, t } = this.props;
+
+    return (
+      <Modal.Actions>
+        <Button.Group>
+          <Button
+            isNegative
+            onClick={onClose}
+            label={t('Cancel')}
+          />
+          <Button.Or />
+          <Button
+            isPrimary
+            onClick={onForget}
+            label={t('Forget')}
+          />
+        </Button.Group>
+      </Modal.Actions>
+    );
+  }
+
+  private renderContent () {
+    const { address, code, mode = 'account' } = this.props;
+
+    switch (mode) {
+      case 'account':
+      case 'address':
+      case 'contract':
+        return (
+          <AddressRow
+            isInline
+            value={address!}
+          >
+            {this.content()}
+          </AddressRow>
+        );
+      case 'code':
+        return (
+          <CodeRow
+            isInline
+            code={code!}
+          >
+            {this.content()}
+          </CodeRow>
+        );
+    }
+  }
+}
+
+export default translate(Forget);

+ 2 - 2
packages/ui-app/src/InputAddress.tsx

@@ -14,7 +14,7 @@ import createItem from '@polkadot/ui-keyring/options/item';
 import { withMulti, withObservable } from '@polkadot/ui-api';
 
 import Dropdown from './Dropdown';
-import { classes, getAddrName } from './util';
+import { classes, getAddressName } from './util';
 import addressToAddress from './util/toAddress';
 
 type Props = BareProps & {
@@ -191,7 +191,7 @@ class InputAddress extends React.PureComponent<Props, State> {
       return undefined;
     }
 
-    return getAddrName(value, true);
+    return getAddressName(value, null, true);
   }
 
   private getLastOptionValue (): KeyringSectionOption | undefined {

+ 40 - 26
packages/ui-app/src/InputFile.tsx

@@ -41,38 +41,50 @@ type LoadEvent = {
 };
 
 class InputFile extends React.PureComponent<Props, State> {
-  state: State = {};
+  dropZone: any;
+  state: State;
+
+  constructor (props: Props) {
+    super(props);
+    this.state = {} as State;
+    this.dropZone = React.createRef();
+  }
 
   render () {
     const { accept, className, clearContent, help, isDisabled, isError = false, label, placeholder, t, withEllipsis, withLabel } = this.props;
     const { file } = this.state;
 
-    return (
+    const dropZone = (
+      <Dropzone
+        accept={accept}
+        className={classes('ui--InputFile', isError ? 'error' : '', className)}
+        disabled={isDisabled}
+        multiple={false}
+        ref={this.dropZone}
+        onDrop={this.onDrop}
+      >
+        <div className='label'>
+          {
+            !file || clearContent
+              ? placeholder || t('click to select or drag and drop the file here')
+              : placeholder || t('{{name}} ({{size}} bytes)', {
+                replace: file
+              })
+          }
+        </div>
+      </Dropzone>
+    );
+
+    return label ? (
       <Labelled
         help={help}
         label={label}
         withEllipsis={withEllipsis}
         withLabel={withLabel}
       >
-        <Dropzone
-          accept={accept}
-          className={classes('ui--InputFile', isError ? 'error' : '', className)}
-          disabled={isDisabled}
-          multiple={false}
-          onDrop={this.onDrop}
-        >
-          <div className='label'>
-            {
-              !file || clearContent
-                ? placeholder || t('click to select or drag and drop the file here')
-                : placeholder || t('{{name}} ({{size}} bytes)', {
-                  replace: file
-                })
-            }
-          </div>
-        </Dropzone>
+        {dropZone}
       </Labelled>
-    );
+    ) : dropZone;
   }
 
   private onDrop = (files: Array<File>) => {
@@ -96,12 +108,14 @@ class InputFile extends React.PureComponent<Props, State> {
 
         onChange && onChange(data, name);
 
-        this.setState({
-          file: {
-            name,
-            size: data.length
-          }
-        });
+        if (this.dropZone && this.dropZone.current) {
+          this.setState({
+            file: {
+              name,
+              size: data.length
+            }
+          });
+        }
       };
 
       reader.readAsArrayBuffer(file);

+ 117 - 0
packages/ui-app/src/Messages.tsx

@@ -0,0 +1,117 @@
+// Copyright 2017-2019 @polkadot/ui-app authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { ContractAbi } from '@polkadot/types';
+import { I18nProps } from '@polkadot/ui-app/types';
+import { Button } from '@polkadot/ui-app';
+
+import React from 'react';
+import styled from 'styled-components';
+import classNames from 'classnames';
+
+import translate from './translate';
+
+export type Props = I18nProps & {
+  address?: string,
+  contractAbi: ContractAbi,
+  isRemovable: boolean,
+  onRemove?: () => void,
+  onSelect?: (callAddress?: string, callMethod?: string) => void
+};
+
+const Wrapper = styled.div`
+  font-size: 0.9rem;
+  min-height: 3.5rem;
+  padding: 0;
+  margin: 0;
+  display: inline-flex;
+  justify-content: flex-start;
+  align-items: center;
+  flex-wrap: wrap;
+
+  & > .message {
+    font-family: monospace;
+    font-weight: normal;
+    margin-bottom: 0;
+    margin-right: 0;
+    padding: 0.5rem;
+    margin: 0;
+    border-radius: 0.7rem;
+
+    &.disabled {
+      opacity: 1 !important;
+      background: #eee !important;
+      color: #555 !important;
+    }
+
+    &:not(:last-of-type) {
+      margin-right: 1rem;
+    }
+  }
+`;
+
+class Messages extends React.PureComponent<Props> {
+
+  render () {
+    const { contractAbi: { abi: { messages } }, isRemovable, onRemove = () => null, onSelect, t } = this.props;
+
+    return (
+      <Wrapper className={onSelect && 'select'}>
+        {messages.map((_, index) => {
+          return this.renderMessage(index);
+        })}
+        {isRemovable && (
+          <Button
+            className='iconButton'
+            icon='remove'
+            onClick={onRemove}
+            size='tiny'
+            isNegative
+            tooltip={t('Remove ABI')}
+          />
+        )}
+      </Wrapper>
+    );
+  }
+
+  renderMessage (index: number) {
+    const { contractAbi: { abi: { messages } }, onSelect } = this.props;
+
+    if (!messages[index]) {
+      return null;
+    }
+
+    const { args, name, return_type: returnType } = messages[index];
+
+    return (
+      <Button
+        key={name}
+        className={classNames('message', !onSelect && 'exempt-hover')}
+        isDisabled={!onSelect}
+        onClick={this.onSelect(index)}
+        isPrimary={!!onSelect}
+      >
+        {name}
+        (
+        {args.map(({ name: argName, type }) => `${argName}: ${type}`).join(', ')}
+        )
+        {returnType && `: ${returnType}`}
+      </Button>
+    );
+  }
+
+  onSelect = (index: number) => () => {
+    const { address: callAddress, contractAbi: { abi: { messages } }, onSelect } = this.props;
+
+    if (!callAddress || !messages || !messages[index]) {
+      return;
+    }
+
+    const { name: callMethod } = messages[index];
+
+    onSelect && onSelect(callAddress, callMethod);
+  }
+}
+
+export default translate(Messages);

+ 1 - 1
packages/ui-app/src/Params/Extrinsic.tsx

@@ -17,7 +17,7 @@ type Props = I18nProps & {
   isDisabled?: boolean,
   isError?: boolean,
   isPrivate: boolean,
-  label: string,
+  label: React.ReactNode,
   onChange?: RawParam$OnChange,
   onEnter?: RawParam$OnEnter,
   withLabel?: boolean

+ 312 - 0
packages/ui-app/src/Row.tsx

@@ -0,0 +1,312 @@
+// Copyright 2017-2019 @polkadot/ui-app authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { Label } from 'semantic-ui-react';
+import React from 'react';
+import { Button, Input, InputTags } from '@polkadot/ui-app';
+
+import { classes } from './util';
+
+export const styles = `
+  text-align: left;
+
+  &.inline {
+    display: flex;
+
+    .ui--Row-children {
+      padding: 0 0 0 3rem;
+    }
+  }
+
+  &.invalid {
+    filter: grayscale(100);
+    opacity: 0.5;
+  }
+
+  button.ui.icon.editButton {
+    padding: 0em .3em .3em .3em;
+    color: #2e86ab;
+    background: none;
+    /*trick to let the button in the flow but keep the content centered regardless*/
+    margin-left: -2em;
+    position: relative;
+    right: -2.3em;
+    z-index: 1;
+  }
+
+  .ui--Row-accountId,
+  .ui--Row-accountIndex {
+    font-family: monospace;
+    font-size: 1.25em;
+    padding: 0;
+    margin-bottom: 0.25rem;
+  }
+
+  .ui--Row-accountIndex {
+    font-style: italic;
+  }
+
+  .ui--Row-balances {
+    .column {
+      display: block;
+
+      label,
+      .result {
+        display: inline-block;
+      }
+    }
+
+    > span {
+      text-align: left;
+    }
+  }
+
+  .ui--Row-base {
+    display: flex;
+  }
+
+  .ui--Row-buttons {
+    flex: 0;
+    margin: -0.75rem -0.75rem 0 0;
+    white-space: nowrap;
+  }
+
+  .ui--Row-children {
+    display: block;
+    padding-top: 1rem;
+  }
+
+  .ui--Row-details {
+    flex: 1;
+    margin-right: 1rem;
+    padding: 0.25rem 0 0;
+
+    * {
+      vertical-align: middle;
+    }
+  }
+
+  .ui--Row-icon {
+    flex: 0;
+    margin-right: 1em;
+    position: relative;
+
+    .ui--Row-icon-info {
+      left: -0.5rem;
+      position: absolute;
+      top: -0.5rem;
+    }
+  }
+
+  .ui--Row-name {
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    text-transform: uppercase;
+    white-space: normal;
+  }
+
+  .ui--Row-name-input {
+    input {
+      height: 1em;
+      text-transform: uppercase;
+      margin-top: -0.3em;
+      margin-bottom: -0.35em;
+    }
+  }
+
+  .ui--Row-tags {
+    &.editable {
+      display: flex;
+      flex-wrap: wrap;
+      justify-content: left;
+
+      > span {
+        border: 1px #00000052 solid;
+        border-radius: .5em;
+        border-style: dashed;
+        color: grey;
+        font-size: x-small;
+        padding: .1em 0.3em 0.1em 0.3em;
+        margin-top: .2em;
+      }
+
+      > div.label {
+        margin-top:.3em
+      }
+    }
+  }
+
+  .ui--Row-tags-input {
+    margin-bottom: -1.4em;
+  }
+`;
+
+export type RowProps = {
+  buttons?: React.ReactNode,
+  children?: React.ReactNode,
+  className?: string,
+  defaultName?: string,
+  extraInfo?: React.ReactNode,
+  iconInfo?: React.ReactNode,
+  isEditable?: boolean,
+  isInline?: boolean,
+  withIcon?: boolean,
+  withTags?: boolean
+};
+
+export type RowState = {
+  isEditingName: boolean,
+  isEditingTags: boolean,
+  name: string,
+  tags: string[]
+};
+
+// const DEFAULT_ADDR = '5'.padEnd(16, 'x');
+// const ICON_SIZE = 48;
+
+class Row<P extends RowProps, S extends RowState> extends React.PureComponent<P, S> {
+  state: S = {
+    isEditingName: false,
+    isEditingTags: false
+  } as S;
+
+  static defaultProps = {
+    defaultName: '<unknown>'
+  };
+
+  protected onChangeName = (name: string) => {
+    this.setState({ name });
+  }
+
+  protected onChangeTags = (tags: string[]) => {
+    this.setState({ tags });
+  }
+
+  protected renderButtons () {
+    const { buttons } = this.props;
+
+    return buttons
+      ? <div className='ui--Row-buttons'>{buttons}</div>
+      : null;
+  }
+
+  protected renderChildren () {
+    const { children } = this.props;
+
+    const hasChildren = !children
+      ? false
+      : Array.isArray(children)
+        ? children.filter((child) => child).length !== 0
+        : true;
+
+    if (!hasChildren) {
+      return null;
+    }
+
+    return (
+      <div className='ui--Row-children'>
+        {children}
+      </div>
+    );
+  }
+
+  protected renderEditIcon () {
+    return (
+      <Button
+        className='iconButton'
+        icon='edit'
+        size='mini'
+        isPrimary
+        key='unlock'
+      />
+    );
+  }
+
+  protected renderName () {
+    const { isEditable } = this.props;
+    const { isEditingName, name } = this.state;
+
+    return isEditingName
+      ? (
+        <Input
+          autoFocus
+          className='ui--Row-name-input'
+          defaultValue={name}
+          onBlur={this.saveName}
+          onChange={this.onChangeName}
+          onEnter={this.saveName}
+          withLabel={false}
+        />
+      )
+      : (
+        <div
+          className={classes('ui--Row-name', isEditable && 'editable')}
+          onClick={isEditable ? this.toggleNameEditor : undefined}
+        >
+          {name}
+          {isEditable && this.renderEditIcon()}
+        </div>
+      );
+  }
+
+  protected renderTags () {
+    const { isEditingTags, tags } = this.state;
+    const { isEditable, withTags = false } = this.props;
+
+    if (!withTags) {
+      return null;
+    }
+
+    return isEditingTags
+      ? (
+        <InputTags
+          className='ui--Row-tags-input'
+          onBlur={this.saveTags}
+          onChange={this.onChangeTags}
+          onClose={this.saveTags}
+          openOnFocus
+          defaultValue = {tags}
+          searchInput={{ autoFocus: true }}
+          value={tags}
+          withLabel={false}
+        />
+      )
+      : (
+        <div
+          className={classes('ui--Row-tags', isEditable && 'editable')}
+          onClick={isEditable ? this.toggleTagsEditor : undefined}
+        >
+          {
+            !tags.length
+              ? (isEditable ? <span className='addTags'>add tags</span> : undefined)
+              : tags.map((tag) => (
+                <Label key={tag} size='tiny' color='grey'>{tag}</Label>
+              ))
+          }
+          {isEditable && this.renderEditIcon()}
+        </div>
+      );
+  }
+
+  protected saveName!: () => void;
+
+  protected saveTags!: () => void;
+
+  protected toggleNameEditor = () => {
+    this.setState(({ isEditingName }) => ({
+      isEditingName: !isEditingName
+    }));
+  }
+
+  protected toggleTagsEditor = () => {
+    this.setState(({ isEditingTags }) => ({
+      isEditingTags: !isEditingTags
+    }));
+  }
+}
+
+export default Row;

+ 37 - 15
packages/ui-app/src/Tabs.tsx

@@ -6,11 +6,28 @@ import { BareProps } from './types';
 
 import React from 'react';
 import { NavLink } from 'react-router-dom';
+import styled from 'styled-components';
 
 import { classes } from './util';
+import Icon from './Icon';
+
+const MyIcon = styled(Icon)`
+  &&& {
+    width: 1rem;
+    margin: 0.7rem 0;
+  }
+`;
+
+const Next = () => (
+  <MyIcon
+    name='arrow right'
+  />
+);
 
 export type TabItem = {
   hasParams?: boolean,
+  isExact?: boolean,
+  isRoot?: boolean,
   name: string,
   text: React.ReactNode
 };
@@ -18,7 +35,8 @@ export type TabItem = {
 type Props = BareProps & {
   basePath: string,
   hidden?: Array<string>,
-  items: Array<TabItem>
+  items: Array<TabItem>,
+  isSequence?: boolean
 };
 
 export default class Tabs extends React.PureComponent<Props> {
@@ -38,26 +56,30 @@ export default class Tabs extends React.PureComponent<Props> {
     );
   }
 
-  private renderItem = ({ hasParams, name, text }: TabItem, index: number) => {
-    const { basePath } = this.props;
-    const to = index === 0
+  private renderItem = ({ hasParams, isRoot, name, text, ...tab }: TabItem, index: number) => {
+    const { basePath, isSequence, items } = this.props;
+    const to = isRoot
       ? basePath
       : `${basePath}/${name}`;
     // only do exact matching when not the fallback (first position tab),
     // params are problematic for dynamic hidden such as app-accounts
-    const isExact = !hasParams || index === 0;
+    const isExact = tab.isExact || !hasParams || (!isSequence && index === 0);
 
     return (
-      <NavLink
-        activeClassName='active'
-        className='item'
-        exact={isExact}
-        key={to}
-        strict={isExact}
-        to={to}
-      >
-        {text}
-      </NavLink>
+      <React.Fragment key={to}>
+        <NavLink
+          activeClassName='active'
+          className='item'
+          exact={isExact}
+          strict={isExact}
+          to={to}
+        >
+          {text}
+        </NavLink>
+        {(isSequence && index < items.length - 1) && (
+          <Next />
+        )}
+      </React.Fragment>
     );
   }
 }

+ 3 - 0
packages/ui-app/src/index.tsx

@@ -18,6 +18,7 @@ export { default as CardSummary } from './CardSummary';
 export { default as Chart } from './Chart';
 export { default as Columar } from './Columar';
 export { default as Column } from './Column';
+export { default as CodeRow } from './CodeRow';
 export { default as CopyButton } from './CopyButton';
 export { default as CryptoType } from './CryptoType';
 export { default as Dropdown } from './Dropdown';
@@ -25,6 +26,7 @@ export { default as Editor } from './Editor';
 export { default as Event } from './Event';
 export { default as Extrinsic } from './Extrinsic';
 export { default as FilterOverlay } from './FilterOverlay';
+export { default as Forget } from './Forget';
 export { default as HelpOverlay } from './HelpOverlay';
 export { default as Icon } from './Icon';
 export { default as IdentityIcon } from './IdentityIcon';
@@ -44,6 +46,7 @@ export { default as LabelHelp } from './LabelHelp';
 export { default as Labelled } from './Labelled';
 export { default as LinkPolkascan } from './LinkPolkascan';
 export { default as Menu } from './Menu';
+export { default as Messages } from './Messages';
 export { default as Modal } from './Modal';
 export { default as Nonce } from './Nonce';
 export { default as Output } from './Output';

+ 1 - 1
packages/ui-app/src/styles/app.css

@@ -108,7 +108,7 @@ article {
   }
 
   &:not(:hover) {
-    .ui.button {
+    .ui.button:not(.disabled) {
       background: #eee !important;
       color: #555 !important;
     }

+ 1 - 0
packages/ui-app/src/styles/semantic.css

@@ -108,6 +108,7 @@
   > .actions {
     border-top: none;
     text-align: right;
+    padding: 1rem !important;
   }
 
   > .header:not(.ui) {

+ 5 - 6
packages/ui-app/src/util/getAddrName.ts → packages/ui-app/src/util/getAddressName.ts

@@ -3,24 +3,23 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import keyring from '@polkadot/ui-keyring';
+import { KeyringItemType } from '@polkadot/ui-keyring/types';
 import toShortAddress from './toShortAddress';
 
-export default function getAddrName (address: string, withShort?: boolean, defaultName?: string): string | undefined {
+export default function getAddressName (address: string, type: KeyringItemType | null = null, withShort?: boolean, defaultName?: string | null): string | undefined {
   let pair;
 
   try {
-    pair = keyring.getAccount(address).isValid()
-      ? keyring.getAccount(address)
-      : keyring.getAddress(address);
+    pair = keyring.getAddress(address, type);
   } catch (error) {
     // all-ok, we have empty fallbacks
   }
 
   const name = pair && pair.isValid()
     ? pair.getMeta().name
-    : defaultName;
+    : (defaultName || null);
 
   return !name && withShort
     ? toShortAddress(address)
-    : name;
+    : (name || '<unknown>');
 }

+ 3 - 4
packages/ui-app/src/util/getAddrTags.ts → packages/ui-app/src/util/getAddressTags.ts

@@ -3,14 +3,13 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import keyring from '@polkadot/ui-keyring';
+import { KeyringItemType } from '@polkadot/ui-keyring/types';
 
-export default function getAddrAtgs (address: string): Array<string> {
+export default function getAddressTags (address: string, type: KeyringItemType = 'account'): Array<string> {
   let pair;
 
   try {
-    pair = keyring.getAccount(address).isValid()
-      ? keyring.getAccount(address)
-      : keyring.getAddress(address);
+    pair = keyring.getAddress(address, type);
   } catch (error) {
     // all-ok, we have empty fallbacks
   }

+ 25 - 0
packages/ui-app/src/util/getContractAbi.ts

@@ -0,0 +1,25 @@
+// Copyright 2017-2019 @polkadot/ui-app authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import keyring from '@polkadot/ui-keyring';
+import { ContractAbi } from '@polkadot/types';
+
+export default function getContractAbi (address: string): ContractAbi | null {
+  let pair;
+
+  try {
+    pair = keyring.getContract(address);
+  } catch (error) {
+    // all-ok, we have empty fallbacks
+  }
+
+  return (
+    pair &&
+    pair.isValid() &&
+    pair.getMeta().contract &&
+    new ContractAbi(
+      JSON.parse(pair.getMeta().contract!.abi)
+    )
+  ) || null;
+}

+ 3 - 2
packages/ui-app/src/util/index.ts

@@ -3,7 +3,8 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 export { default as classes } from './classes';
-export { default as getAddrName } from './getAddrName';
-export { default as getAddrTags } from './getAddrTags';
+export { default as getAddressName } from './getAddressName';
+export { default as getAddressTags } from './getAddressTags';
+export { default as getContractAbi } from './getContractAbi';
 export { default as toAddress } from './toAddress';
 export { default as toShortAddress } from './toShortAddress';

+ 1 - 0
packages/ui-params/src/Param/Account.tsx

@@ -29,6 +29,7 @@ export default class Account extends React.PureComponent<Props> {
           label={label}
           onChange={this.onChange}
           placeholder='5...'
+          type='allPlus'
           withEllipsis
           withLabel={withLabel}
         />

+ 1 - 1
packages/ui-params/src/Param/Base.tsx

@@ -13,7 +13,7 @@ import Bare from './Bare';
 type Props = BareProps & {
   children: React.ReactNode,
   isDisabled?: boolean,
-  label?: string,
+  label?: React.ReactNode,
   size?: Size,
   withLabel?: boolean
 };

+ 1 - 1
packages/ui-params/src/Param/File.tsx

@@ -13,7 +13,7 @@ type Props = BareProps & {
   defaultValue?: any,
   isDisabled?: boolean,
   isError?: boolean,
-  label: string,
+  label?: React.ReactNode,
   onChange?: (contents: Uint8Array) => void,
   placeholder?: string,
   withLabel?: boolean

+ 10 - 3
packages/ui-params/src/Param/index.tsx

@@ -6,12 +6,17 @@ import { I18nProps } from '@polkadot/ui-app/types';
 import { BaseProps, Props as ComponentProps, ComponentMap } from '../types';
 
 import React from 'react';
+import styled from 'styled-components';
 import { classes } from '@polkadot/ui-app/util';
 import translate from '@polkadot/ui-app/translate';
 import { isUndefined } from '@polkadot/util';
 
 import findComponent from './findComponent';
 
+const Type = styled.span`
+  font-family: monospace;
+`;
+
 type Props = I18nProps & BaseProps & {
   isDisabled?: boolean,
   overrides?: ComponentMap
@@ -50,9 +55,11 @@ class ParamComponent extends React.PureComponent<Props, State> {
         key={`${name}:${type}`}
         isDisabled={isDisabled}
         label={
-          isUndefined(name)
-            ? type.type
-            : `${name}: ${type.type}`
+          <Type>
+            {isUndefined(name)
+              ? type.type
+              : `${name}: ${type.type}`}
+          </Type>
         }
         name={name}
         onChange={onChange}

+ 1 - 1
packages/ui-params/src/types.ts

@@ -36,7 +36,7 @@ export type Props = BaseProps & {
   isDisabled?: boolean,
   isError?: boolean,
   isReadOnly?: boolean,
-  label: string,
+  label?: React.ReactNode,
   withLabel?: boolean
 };
 

+ 1 - 2
packages/ui-signer/src/Modal.tsx

@@ -16,7 +16,6 @@ import { web3FromSource } from '@polkadot/extension-dapp';
 import { Button, Modal } from '@polkadot/ui-app';
 import { withApi, withMulti, withObservable } from '@polkadot/ui-api';
 import keyring from '@polkadot/ui-keyring';
-import accountObservable from '@polkadot/ui-keyring/observable/accounts';
 import { assert, isFunction } from '@polkadot/util';
 import { format } from '@polkadot/util/logger';
 
@@ -399,5 +398,5 @@ export default withMulti(
   Signer,
   translate,
   withApi,
-  withObservable(accountObservable.subject, { propName: 'allAccounts' })
+  withObservable(keyring.accounts.subject, { propName: 'allAccounts' })
 );

+ 2 - 0
tsconfig.json

@@ -41,6 +41,8 @@
       "@polkadot/ui-api/*": [ "packages/ui-api/src/*" ],
       "@polkadot/ui-app": [ "packages/ui-app/src" ],
       "@polkadot/ui-app/*": [ "packages/ui-app/src/*" ],
+      "@polkadot/ui-contracts": [ "packages/ui-contracts/src" ],
+      "@polkadot/ui-contracts/*": [ "packages/ui-contracts/src/*" ],
       "@polkadot/ui-params": [ "packages/ui-params/src" ],
       "@polkadot/ui-params/*": [ "packages/ui-params/src/*" ],
       "@polkadot/ui-reactive": [ "packages/ui-reactive/src" ],

+ 65 - 65
yarn.lock

@@ -1761,31 +1761,31 @@
     universal-user-agent "^2.0.0"
     url-template "^2.0.8"
 
-"@polkadot/api-derive@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.81.0-beta.3.tgz#fe3c3232deab47b79a3ad26ff1b153810e350e14"
-  integrity sha512-WGieCK7FaRUz2kiuKXfPTsOSv1clHVtEcAZhRNoA95+GhFbhQ9RIEQ7XNdU/CsC+SRN+P2NAaW3tqyOzbe/lhw==
+"@polkadot/api-derive@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.81.0-beta.8.tgz#4adec076866bfac87d396d5f40dff35795c33c25"
+  integrity sha512-pKNiSvHSSAvNWVH/0wVLbMflCCNXvJ/zSKhAMfi7OyQJvTJktLkZ2yqivMcCUiU3KWhUmM3x3AFbBxCR7BqrHw==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/api" "^0.81.0-beta.3"
-    "@polkadot/keyring" "^0.93.0-beta.0"
-    "@polkadot/types" "^0.81.0-beta.3"
+    "@polkadot/api" "^0.81.0-beta.8"
+    "@polkadot/keyring" "^0.93.0-beta.1"
+    "@polkadot/types" "^0.81.0-beta.8"
     "@types/memoizee" "^0.4.2"
     memoizee "^0.4.14"
 
-"@polkadot/api@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.81.0-beta.3.tgz#ffbbe0d97d22a015932047de6655e74670d5b8f0"
-  integrity sha512-5plATjxvOtkBLIWFseahXz7t4s1njG0ZQ5nFcdyFlGPKElluPoyH3tvpbaSNvfgwuRoOLpGwNRUoNnVMuM2t+Q==
+"@polkadot/api@^0.81.0-beta.7", "@polkadot/api@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.81.0-beta.8.tgz#059e4c8f4ef93ee224d459af5b8e6c19b407b9a1"
+  integrity sha512-JDmvIQ+084wP/EXh3PzpZscrVAlkNJV4ZHblVckNN86KlBj+AX0Ky/SxtLqSjGg7xAFjAcAOJ/26rDcrQctmEA==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/api-derive" "^0.81.0-beta.3"
-    "@polkadot/extrinsics" "^0.81.0-beta.3"
-    "@polkadot/rpc-provider" "^0.81.0-beta.3"
-    "@polkadot/rpc-rx" "^0.81.0-beta.3"
-    "@polkadot/storage" "^0.81.0-beta.3"
-    "@polkadot/types" "^0.81.0-beta.3"
-    "@polkadot/util-crypto" "^0.93.0-beta.0"
+    "@polkadot/api-derive" "^0.81.0-beta.8"
+    "@polkadot/extrinsics" "^0.81.0-beta.8"
+    "@polkadot/rpc-provider" "^0.81.0-beta.8"
+    "@polkadot/rpc-rx" "^0.81.0-beta.8"
+    "@polkadot/storage" "^0.81.0-beta.8"
+    "@polkadot/types" "^0.81.0-beta.8"
+    "@polkadot/util-crypto" "^0.93.0-beta.1"
 
 "@polkadot/dev-react@^0.30.0-beta.11":
   version "0.30.0-beta.11"
@@ -1865,23 +1865,23 @@
   resolved "https://registry.yarnpkg.com/@polkadot/extension-dapp/-/extension-dapp-0.1.1-beta.25.tgz#16c435127fbc37cb04c7179fcf557da51e14fe00"
   integrity sha512-SrZvB5YQgv6DWd+rsaMKgBvHeDeToQ9fY3iZ86xcyi5NromH+yX3z0kdne9lxWJRwr8v/haqwGpKxQbbxLvaVQ==
 
-"@polkadot/extrinsics@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/extrinsics/-/extrinsics-0.81.0-beta.3.tgz#0c08964df1a5d6db61177bd84cf0e8c6373501ba"
-  integrity sha512-HImQGrDiJ0QoYMxDvovyybU3OVTZV9k9M2s5YtMb60mHza0Bx8ceSq2BpiyfGuQvL1sv3HVestb1ID31znDhsA==
+"@polkadot/extrinsics@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/extrinsics/-/extrinsics-0.81.0-beta.8.tgz#873db3a6456d2d0fd18e1a237c2b23cdb1beb88e"
+  integrity sha512-eayCwuLDCyCaMtH06SRARYxxVNnFx7hQQ7cC2l8EtwMIj/CWVUlDszv63aTnFBJx1WSKx8n58ZNYmL7SaW65hQ==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/types" "^0.81.0-beta.3"
-    "@polkadot/util" "^0.93.0-beta.0"
+    "@polkadot/types" "^0.81.0-beta.8"
+    "@polkadot/util" "^0.93.0-beta.1"
 
-"@polkadot/jsonrpc@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.81.0-beta.3.tgz#e6fdaf70c5737748594e2eb4e7aa1df7cc38cab3"
-  integrity sha512-iXbIwPsA4Ed5Kag4XRu6ExGkYGjErfLWgXdlSwGupwrNWpsk8bdCGVuHMW7SMPaaEKyL5EXvdUL/C4ZE1cnptg==
+"@polkadot/jsonrpc@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.81.0-beta.8.tgz#c5a09cd8c6c5b180c8da8db37b0d346d25edf22b"
+  integrity sha512-Jrzl3rwYaI2YEfLufxwOGVvx00fYlnj+YUVY5rBbaFaFgPtAh8vcWQfa8697eCWFz/pPYfRbwf+qTjDeYK6WTQ==
   dependencies:
     "@babel/runtime" "^7.4.5"
 
-"@polkadot/keyring@^0.93.0-beta.0":
+"@polkadot/keyring@^0.93.0-beta.0", "@polkadot/keyring@^0.93.0-beta.1":
   version "0.93.0-beta.0"
   resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-0.93.0-beta.0.tgz#4c0491141814c70643cf7e7bdc307f91372dc11f"
   integrity sha512-GUvIsyo4b4ZfPjb5XnshjUZ9GmDJRYE6uSs7Ytg0tlPsl/AL4awYGmbVGiWqJL27gMwxOTUXIQ/dVpUXVFk5qg==
@@ -1892,53 +1892,53 @@
     "@types/bs58" "^4.0.0"
     bs58 "^4.0.1"
 
-"@polkadot/rpc-core@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.81.0-beta.3.tgz#1ae9e3f8483e427f3a5b41be9dfdda27a674cf44"
-  integrity sha512-jPUxNOawzyHSMJ75W8hKhkrZBJHZSVIZ42BlIzVl9iWob0Vfsbgvb9nON+5cDU7k1VAxu0Hvxr3bOoSHadDMjA==
+"@polkadot/rpc-core@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.81.0-beta.8.tgz#3240fbac6868f2c23b1e5e2cd008d88fb5f62cca"
+  integrity sha512-aERBdr+Wi3pgTrEVJS0D+MKc/KnOYytE09r0B7HrNH511wpOVv1fdcjnZbwwqUuBR9GeNpB7qN8ipMrhx4pAhQ==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/jsonrpc" "^0.81.0-beta.3"
-    "@polkadot/rpc-provider" "^0.81.0-beta.3"
-    "@polkadot/types" "^0.81.0-beta.3"
-    "@polkadot/util" "^0.93.0-beta.0"
+    "@polkadot/jsonrpc" "^0.81.0-beta.8"
+    "@polkadot/rpc-provider" "^0.81.0-beta.8"
+    "@polkadot/types" "^0.81.0-beta.8"
+    "@polkadot/util" "^0.93.0-beta.1"
 
-"@polkadot/rpc-provider@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.81.0-beta.3.tgz#97ff3b837aaa806ab4bed2858bf57bb46f87ead2"
-  integrity sha512-dpR1LZvb3gfhrhoGoz+m94btkb/VZOg10tBbYoF0ViV4Pt/C/P9aqYsGmjGKVOYoyXpt7X5d2/IVqIPsWHTccA==
+"@polkadot/rpc-provider@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.81.0-beta.8.tgz#e48ffe6fc3287a575db2df1d3fc9a4a7088023db"
+  integrity sha512-9g/2tQbx/9d97Ikyc+LCJ9i/LhDga4UzAwD7lIpPx0rm6A/2/ZC93W7gt7EmKroG3t4csvJCyigz/MfG/cGl3w==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/storage" "^0.81.0-beta.3"
-    "@polkadot/util" "^0.93.0-beta.0"
-    "@polkadot/util-crypto" "^0.93.0-beta.0"
+    "@polkadot/storage" "^0.81.0-beta.8"
+    "@polkadot/util" "^0.93.0-beta.1"
+    "@polkadot/util-crypto" "^0.93.0-beta.1"
     "@types/nock" "^10.0.3"
     eventemitter3 "^3.1.0"
     isomorphic-fetch "^2.2.1"
     websocket "^1.0.28"
 
-"@polkadot/rpc-rx@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/rpc-rx/-/rpc-rx-0.81.0-beta.3.tgz#2d79ac90761d522eba6ab91e6c8a1c85f46b2d6f"
-  integrity sha512-9grI4CT+HM9oFIQngVwGIuq0aoWxzLuSfv3RtYvkOU24lg+3M+3tfyh0/Y75gwpRKep9+xAzNQZYLYAiPZKlPg==
+"@polkadot/rpc-rx@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/rpc-rx/-/rpc-rx-0.81.0-beta.8.tgz#08c5248ba3e3736d5a6a13ba69deb130ed61a3e1"
+  integrity sha512-EyEsh5+HEN3NbTcl6iM7L7FrXJp4RejvTyiRQLHWrQXpODxIBkrjIE5HfKKI9gaqFQuGuB9Ucbqm2bX1rbEPpA==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/rpc-core" "^0.81.0-beta.3"
-    "@polkadot/rpc-provider" "^0.81.0-beta.3"
+    "@polkadot/rpc-core" "^0.81.0-beta.8"
+    "@polkadot/rpc-provider" "^0.81.0-beta.8"
     "@types/memoizee" "^0.4.2"
     "@types/rx" "^4.1.1"
     memoizee "^0.4.14"
     rxjs "^6.5.2"
 
-"@polkadot/storage@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.81.0-beta.3.tgz#2e533adc4bd833c3074626845bbe0cecd95b5e75"
-  integrity sha512-4tnRcnS1+Yhr+m9Ju1VWjHb1zmiBLlKT0m7rQHFQlzO1Pimg62F27l1WzeHSj2d8HXZ8M0UZqdXw9biZyk9wtw==
+"@polkadot/storage@^0.81.0-beta.8":
+  version "0.81.0-beta.8"
+  resolved "https://registry.yarnpkg.com/@polkadot/storage/-/storage-0.81.0-beta.8.tgz#59a037d76d7ceb1f84dd944734f920f18ff6afc6"
+  integrity sha512-YXsNjsML78l7kbCgCzK+tFkBwKnfalP7lY3fYY3JzTXEKf1RhXPwmLGP9w88cwifje7qqItx0928wb6sHpOsRw==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/types" "^0.81.0-beta.3"
-    "@polkadot/util" "^0.93.0-beta.0"
-    "@polkadot/util-crypto" "^0.93.0-beta.0"
+    "@polkadot/types" "^0.81.0-beta.8"
+    "@polkadot/util" "^0.93.0-beta.1"
+    "@polkadot/util-crypto" "^0.93.0-beta.1"
 
 "@polkadot/ts@^0.1.59":
   version "0.1.59"
@@ -1947,14 +1947,14 @@
   dependencies:
     "@types/chrome" "^0.0.86"
 
-"@polkadot/types@^0.81.0-beta.3":
-  version "0.81.0-beta.3"
-  resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.81.0-beta.3.tgz#07fb650ce70cc190768c11831ea43c067795adca"
-  integrity sha512-OVHlXcROpz8ZM3TwAfLMobn+JAlxG+1jQPIG8VYvhvwog1gntJN1L/VIID/Se8r7dNStBnpWkN+IP4j3Wykl3A==
+"@polkadot/types@^0.81.0-beta.7", "@polkadot/types@^0.81.0-beta.8":
+  version "0.81.0-beta.7"
+  resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.81.0-beta.7.tgz#114604229be106075cd720c0a8d6c49a51ebe0e3"
+  integrity sha512-oFKAyPl5uxwh+ffhPBJXnfLGqm+jAyyBFmyhN6mbjEg71ckabyIl15WoRX1wXQQbRaC+gSvmJTMTAtYrF+bh1A==
   dependencies:
     "@babel/runtime" "^7.4.5"
-    "@polkadot/keyring" "^0.93.0-beta.0"
-    "@polkadot/util" "^0.93.0-beta.0"
+    "@polkadot/keyring" "^0.93.0-beta.1"
+    "@polkadot/util" "^0.93.0-beta.1"
 
 "@polkadot/ui-assets@^0.41.0-beta.4":
   version "0.41.0-beta.4"
@@ -1999,7 +1999,7 @@
     "@types/store" "^2.0.2"
     store "^2.0.12"
 
-"@polkadot/util-crypto@^0.93.0-beta.0":
+"@polkadot/util-crypto@^0.93.0-beta.0", "@polkadot/util-crypto@^0.93.0-beta.1":
   version "0.93.0-beta.0"
   resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-0.93.0-beta.0.tgz#dac65de4385fd5f66e399c51a715da9cdd4f6271"
   integrity sha512-CL0LYzWHVWU9jGNIjSYoMHYPX5qrSMwt2WAotks6KnAtq2wg5eIJPDpGpgmDYjDRFlnWUgTsH5cvtRH18+71HA==
@@ -2018,7 +2018,7 @@
     tweetnacl "^1.0.1"
     xxhashjs "^0.2.2"
 
-"@polkadot/util@^0.93.0-beta.0":
+"@polkadot/util@^0.93.0-beta.0", "@polkadot/util@^0.93.0-beta.1":
   version "0.93.0-beta.0"
   resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-0.93.0-beta.0.tgz#78702aff2bebdc3ce3ecb04c42f764086b9c3ac1"
   integrity sha512-tqha4P5kbBzwCE0C0Zyp/QBB5Ym9lnlMa8OQxvWLfrS9ijfvHZbiGZqNHYKTXXClWKM2eBaIuNgEZolBAWC2/g==