Browse Source

Contract.call RPC interface with return type (#1780)

* contracts api + rpc

* finalized

* tweak

* api bump

* linting
Keith Ingram 5 years ago
parent
commit
c2543ae9b2

+ 3 - 3
package.json

@@ -10,10 +10,10 @@
     "packages/*"
   ],
   "resolutions": {
-    "@polkadot/api": "^0.96.0-beta.16",
-    "@polkadot/api-contract": "^0.96.0-beta.16",
+    "@polkadot/api": "^0.96.0-beta.17",
+    "@polkadot/api-contract": "^0.96.0-beta.17",
     "@polkadot/keyring": "^1.6.1",
-    "@polkadot/types": "^0.96.0-beta.16",
+    "@polkadot/types": "^0.96.0-beta.17",
     "@polkadot/util": "^1.6.1",
     "@polkadot/util-crypto": "^1.6.1",
     "babel-core": "^7.0.0-bridge.0",

+ 1 - 1
packages/app-contracts/package.json

@@ -11,7 +11,7 @@
   "license": "Apache-2.0",
   "dependencies": {
     "@babel/runtime": "^7.6.3",
-    "@polkadot/api-contract": "^0.96.0-beta.16",
+    "@polkadot/api-contract": "^0.96.0-beta.17",
     "@polkadot/react-components": "^0.37.0-beta.28"
   }
 }

+ 152 - 148
packages/app-contracts/src/Contracts/Call.tsx

@@ -2,63 +2,76 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
+import { ContractCallOutcome } from '@polkadot/api-contract/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { BareProps, CallContract, I18nProps, StringOrNull } from '@polkadot/react-components/types';
-import { QueueProps } from '@polkadot/react-components/Status/types';
+import { BareProps, I18nProps, StringOrNull } from '@polkadot/react-components/types';
 import { ContractExecResult } from '@polkadot/types/interfaces/contracts';
 
 import BN from 'bn.js';
-import React, { useState } from 'react';
-import rpc from '@polkadot/jsonrpc';
-import { Button, Dropdown, InputAddress, InputBalance, InputNumber, Modal, Output, TxButton } from '@polkadot/react-components';
-import { QueueConsumer } from '@polkadot/react-components/Status/Context';
+import React, { useState, useEffect } from 'react';
+import styled from 'styled-components';
+import { Button, Dropdown, IconLink, InputAddress, InputBalance, InputNumber, Modal, Toggle, TxButton } from '@polkadot/react-components';
+import { PromiseContract as ApiContract } from '@polkadot/api-contract';
 import { withApi, withMulti } from '@polkadot/react-api';
-import { isNull, isUndefined } from '@polkadot/util';
+import { createValue } from '@polkadot/react-params/values';
+import { isNull } from '@polkadot/util';
 
 import Params from '../Params';
+import Outcome from './Outcome';
 
 import translate from '../translate';
 import { GAS_LIMIT } from '../constants';
-import { findCallMethod, getContractForAddress, getCallMethodOptions, getContractMethodFn } from './util';
+import { getCallMessageOptions } from './util';
 
 interface Props extends BareProps, I18nProps, ApiProps {
-  callContract: CallContract | null;
-  callMethodIndex: number | null;
+  callContract: ApiContract | null;
+  callMessageIndex: number | null;
+  callResults: ContractExecResult[];
   isOpen: boolean;
-  onChangeCallContract: (callContract: CallContract) => void;
-  onChangeCallMethodIndex: (callMethodIndex: number) => void;
+  onChangeCallContractAddress: (callContractAddress: StringOrNull) => void;
+  onChangeCallMessageIndex: (callMessageIndex: number) => void;
   onClose: () => void;
 }
 
 function Call (props: Props): React.ReactElement<Props> | null {
-  const { isOpen, callContract, callMethodIndex, onChangeCallContract, onChangeCallMethodIndex, onClose, api, t } = props;
+  const { className, isOpen, callContract, callMessageIndex, onChangeCallContractAddress, onChangeCallMessageIndex, onClose, api, t } = props;
 
-  if (isNull(callContract) || isNull(callMethodIndex)) {
+  if (isNull(callContract) || isNull(callMessageIndex)) {
     return null;
   }
 
   const hasRpc = api.rpc.contracts && api.rpc.contracts.call;
-  const callMethod = findCallMethod(callContract, callMethodIndex);
-  const useRpc = hasRpc && callMethod && !callMethod.mutates;
-  // const isRpc = false;
+  let callMessage = callContract.getMessage(callMessageIndex);
 
   const [accountId, setAccountId] = useState<StringOrNull>(null);
   const [endowment, setEndowment] = useState<BN>(new BN(0));
   const [gasLimit, setGasLimit] = useState<BN>(new BN(GAS_LIMIT));
   const [isBusy, setIsBusy] = useState(false);
-  const [params, setParams] = useState<any[]>([]);
+  const [outcomes, setOutcomes] = useState<ContractCallOutcome[]>([]);
+  const [params, setParams] = useState<any[]>(callMessage ? callMessage.def.args.map(({ type }): any => createValue({ type })) : []);
+  const [useRpc, setUseRpc] = useState(callMessage && !callMessage.def.mutates);
 
-  const _onChangeAccountId = (accountId: StringOrNull): void => setAccountId(accountId);
+  useEffect((): void => {
+    callMessage = callContract.getMessage(callMessageIndex);
 
-  const _onChangeCallAddress = (callAddress: StringOrNull): void => {
-    const callContract = getContractForAddress(callAddress);
+    setParams(callMessage ? callMessage.def.args.map(({ type }): any => createValue({ type })) : []);
+    if (!callMessage || callMessage.def.mutates) {
+      setUseRpc(false);
+    } else {
+      setUseRpc(true);
+    }
+  }, [callContract, callMessageIndex]);
 
-    onChangeCallContract && callContract.abi && onChangeCallContract(callContract);
-  };
+  useEffect((): void => {
+    setOutcomes([]);
+  }, [callContract]);
 
-  const _onChangeCallMethodString = (callMethodString: string): void => {
-    setParams([]);
-    onChangeCallMethodIndex && onChangeCallMethodIndex(parseInt(callMethodString, 10) || 0);
+  const _onChangeAccountId = (accountId: StringOrNull): void => setAccountId(accountId);
+
+  const _onChangeCallMessageIndexString = (callMessageIndexString: string): void => {
+    onChangeCallMessageIndex && onChangeCallMessageIndex(
+      parseInt(callMessageIndexString, 10) || 0
+    );
   };
 
   const _onChangeEndowment = (endowment?: BN): void => endowment && setEndowment(endowment);
@@ -68,28 +81,29 @@ function Call (props: Props): React.ReactElement<Props> | null {
   const _toggleBusy = (): void => setIsBusy(!isBusy);
 
   const _constructTx = (): any[] => {
-    const fn = getContractMethodFn(callContract, callMethod);
-    if (!fn || !callContract || !callContract.address) {
+    if (!accountId || !callMessage || !callMessage.fn || !callContract || !callContract.address) {
       return [];
     }
 
-    return [callContract.address, endowment, gasLimit, fn(...params)];
+    return [callContract.address.toString(), endowment, gasLimit, callMessage.fn(...params)];
   };
 
-  const _constructRpc = (): [any] | null => {
-    const fn = getContractMethodFn(callContract, callMethod);
-    if (!fn || !accountId || !callContract || !callContract.address || !callContract.abi || !callMethod) {
-      return null;
-    }
-    return [
-      {
-        origin: accountId,
-        dest: callContract.address,
-        value: endowment,
-        gasLimit,
-        inputData: fn(...params)
-      }
-    ];
+  const _onSubmitRpc = (): void => {
+    if (!accountId) return;
+
+    callContract
+      .call('rpc', callMessage.def.name, endowment, gasLimit, ...params)
+      .send(accountId)
+      .then(
+        (outcome: ContractCallOutcome): void => {
+          setOutcomes([outcome, ...outcomes]);
+        }
+      );
+  };
+
+  const _onClearOutcomes = (): void => setOutcomes([]);
+  const _onClearOutcome = (outcomeIndex: number) => (): void => {
+    setOutcomes(outcomes.slice(0, outcomeIndex).concat(outcomes.slice(outcomeIndex + 1)));
   };
 
   const isEndowmentValid = true;
@@ -98,7 +112,7 @@ function Call (props: Props): React.ReactElement<Props> | null {
 
   return (
     <Modal
-      className='app--contracts-Modal'
+      className={[className || '', 'app--contracts-Modal'].join(' ')}
       dimmer='inverted'
       onClose={onClose}
       open={isOpen}
@@ -122,27 +136,28 @@ function Call (props: Props): React.ReactElement<Props> | null {
               help={t('A deployed contract that has either been deployed or attached. The address and ABI are used to construct the parameters.')}
               isDisabled={isBusy}
               label={t('contract to use')}
-              onChange={_onChangeCallAddress}
+              onChange={onChangeCallContractAddress}
               type='contract'
-              value={callContract.address}
+              value={callContract.address.toString()}
             />
-            {callMethodIndex !== null && (
+            {callMessageIndex !== null && (
               <>
                 <Dropdown
+                  defaultValue={`${callMessage.index}`}
                   help={t('The message to send to this contract. Parameters are adjusted based on the ABI provided.')}
                   isDisabled={isBusy}
-                  isError={callMethod === null}
+                  isError={callMessage === null}
                   label={t('message to send')}
-                  onChange={_onChangeCallMethodString}
-                  options={getCallMethodOptions(callContract)}
-                  value={`${callMethodIndex}`}
+                  onChange={_onChangeCallMessageIndexString}
+                  options={getCallMessageOptions(callContract)}
+                  value={`${callMessage.index}`}
                 />
                 <Params
                   isDisabled={isBusy}
                   onChange={_onChangeParams}
                   params={
-                    callMethod
-                      ? callMethod.args
+                    callMessage
+                      ? callMessage.def.args
                       : undefined
                   }
                 />
@@ -167,109 +182,98 @@ function Call (props: Props): React.ReactElement<Props> | null {
             />
           </div>
         )}
-        <QueueConsumer>
-          {
-            ({ queueRpc, txqueue }: QueueProps): React.ReactNode => {
-              const _onSubmitRpc = (): void => {
-                const values = _constructRpc();
-
-                if (values) {
-                  queueRpc({
-                    accountId,
-                    rpc: rpc.contracts.methods.call,
-                    values
-                  });
-                }
-              };
-
-              const results = txqueue
-                .filter(({ error, result, rpc, values }): boolean =>
-                  ((!isUndefined(error) || !isUndefined(result)) &&
-                  rpc.section === 'contracts' && rpc.method === 'call' && !!values && values[0].dest === callContract.address)
-                )
-                .reverse();
-
-              return (
-                <>
-                  <Button.Group>
-                    <Button
-                      icon='cancel'
-                      isNegative
-                      onClick={onClose}
-                      label={t('Cancel')}
-                    />
-                    <Button.Or />
-                    {useRpc
-                      ? (
-                        <Button
-                          icon='sign-in'
-                          isDisabled={!isValid}
-                          isPrimary
-                          label={t('Call')}
-                          onClick={_onSubmitRpc}
-                        />
-                      )
-                      : (
-                        <TxButton
-                          accountId={accountId}
-                          icon='sign-in'
-                          isDisabled={!isValid}
-                          isPrimary
-                          label={t('Call')}
-                          onClick={_toggleBusy}
-                          onFailed={_toggleBusy}
-                          onSuccess={_toggleBusy}
-                          params={_constructTx}
-                          tx={api.tx.contracts ? 'contracts.call' : 'contract.call'}
-                        />
-                      )
-                    }
-                  </Button.Group>
-                  {results.length > 0 && (
-                    <>
-                      <h3>{t('Call results')}</h3>
-                      <div>
-                        {
-                          results.map(
-                            (tx, index): React.ReactNode => {
-                              let output: string;
-                              const contractExecResult = tx.result as ContractExecResult;
-                              if (contractExecResult.isSuccess) {
-                                const { data } = contractExecResult.asSuccess;
-                                output = data.toHex();
-                              } else {
-                                output = 'Error';
-                              }
-
-                              return (
-                                <Output
-                                  isError={contractExecResult.isError}
-                                  key={`result-${tx.id}`}
-                                  label={t(`#${results.length - 1 - index}`)}
-                                  style={{ fontFamily: 'monospace' }}
-                                  value={output}
-                                  withCopy
-                                  withLabel
-                                />
-                              );
-                            }
-                          )
-                        }
-                      </div>
-                    </>
-                  )}
-                </>
-              );
+        {hasRpc && (
+          <Toggle
+            className='rpc-toggle'
+            isDisabled={!!callMessage && callMessage.def.mutates}
+            label={
+              useRpc
+                ? t('send as RPC call')
+                : t('send as transaction')
             }
+            onChange={setUseRpc}
+            value={useRpc || false}
+          />
+        )}
+        <Button.Group>
+          <Button
+            icon='cancel'
+            isNegative
+            onClick={onClose}
+            label={t('Cancel')}
+          />
+          <Button.Or />
+          {useRpc
+            ? (
+              <Button
+                icon='sign-in'
+                isDisabled={!isValid}
+                isPrimary
+                label={t('Call')}
+                onClick={_onSubmitRpc}
+              />
+            )
+            : (
+              <TxButton
+                accountId={accountId}
+                icon='sign-in'
+                isDisabled={!isValid}
+                isPrimary
+                label={t('Call')}
+                onClick={_toggleBusy}
+                onFailed={_toggleBusy}
+                onSuccess={_toggleBusy}
+                params={_constructTx}
+                tx={api.tx.contracts ? 'contracts.call' : 'contract.call'}
+              />
+            )
           }
-        </QueueConsumer>
+        </Button.Group>
+        {outcomes.length > 0 && (
+          <>
+            <h3>
+              {t('Call results')}
+              <IconLink
+                className='clear-all'
+                icon='close'
+                label={t('Clear all')}
+                onClick={_onClearOutcomes}
+              />
+            </h3>
+            <div>
+              {
+                outcomes.map(
+                  (outcome, index): React.ReactNode => {
+                    return (
+                      <Outcome
+                        key={`outcome-${index}`}
+                        onClear={_onClearOutcome(index)}
+                        outcome={outcome}
+                      />
+                    );
+                  }
+                )
+              }
+            </div>
+          </>
+        )}
       </Modal.Content>
     </Modal>
   );
 }
 
 export default withMulti(
-  Call,
+  styled(Call)`
+    .rpc-toggle {
+      margin-top: 1rem;
+      display: flex;
+      justify-content: flex-end;
+    }
+
+    .clear-all {
+      float: right;
+    }
+  `,
   translate,
   withApi
 );

+ 10 - 12
packages/app-contracts/src/Contracts/Contract.tsx

@@ -3,21 +3,22 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { ActionStatus } from '@polkadot/react-components/Status/types';
-import { CallContract, I18nProps } from '@polkadot/react-components/types';
+import { I18nProps } from '@polkadot/react-components/types';
 
 import React, { useState } from 'react';
 import styled from 'styled-components';
 import { RouteComponentProps } from 'react-router';
 import { withRouter } from 'react-router-dom';
 import keyring from '@polkadot/ui-keyring';
+import { PromiseContract as ApiContract } from '@polkadot/api-contract';
 import { AddressRow, Button, Card, Forget, Messages } from '@polkadot/react-components';
 
 import translate from '../translate';
 
 interface Props extends I18nProps, RouteComponentProps {
   basePath: string;
-  contract: CallContract;
-  onCall: (_: CallContract) => (_?: number) => () => void;
+  contract: ApiContract;
+  onCall: (_?: number) => () => void;
 }
 
 const ContractCard = styled(Card)`
@@ -28,7 +29,7 @@ const ContractCard = styled(Card)`
 `;
 
 function Contract (props: Props): React.ReactElement<Props> | null {
-  const { contract, contract: { abi, address }, onCall, t } = props;
+  const { contract: { abi, address }, onCall, t } = props;
 
   if (!address || !abi) {
     return null;
@@ -48,7 +49,7 @@ function Contract (props: Props): React.ReactElement<Props> | null {
     };
 
     try {
-      keyring.forgetContract(address);
+      keyring.forgetContract(address.toString());
       status.status = 'success';
       status.message = t('address forgotten');
     } catch (error) {
@@ -58,15 +59,12 @@ function Contract (props: Props): React.ReactElement<Props> | null {
     _toggleForget();
   };
 
-  const _onCallMessage = onCall(contract);
-  const _onCall = _onCallMessage();
-
   return (
     <ContractCard>
       {
         isForgetOpen && (
           <Forget
-            address={address}
+            address={address.toString()}
             mode='contract'
             onForget={_onForget}
             key='modal-forget-contract'
@@ -88,7 +86,7 @@ function Contract (props: Props): React.ReactElement<Props> | null {
               icon='play'
               isPrimary
               label={t('execute')}
-              onClick={_onCall}
+              onClick={onCall()}
               size='small'
               tooltip={t('Call a method on this contract')}
             />
@@ -105,10 +103,10 @@ function Contract (props: Props): React.ReactElement<Props> | null {
         <details>
           <summary>{t('Messages')}</summary>
           <Messages
-            address={address}
+            address={address.toString()}
             contractAbi={abi}
             isRemovable={false}
-            onSelect={_onCallMessage}
+            onSelect={onCall}
           />
         </details>
       </AddressRow>

+ 103 - 0
packages/app-contracts/src/Contracts/Outcome.tsx

@@ -0,0 +1,103 @@
+// 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 { ContractCallOutcome } from '@polkadot/api-contract/types';
+
+import React from 'react';
+import styled from 'styled-components';
+import { AddressMini, Button, MessageSignature, Output } from '@polkadot/react-components';
+
+interface Props {
+  className?: string;
+  onClear?: () => void;
+  outcome: ContractCallOutcome;
+}
+
+function Outcome (props: Props): React.ReactElement<Props> | null {
+  const { className, onClear, outcome: { message, origin, output, params, success, time } } = props;
+  const dateTime = new Date(time);
+
+  return (
+    <div className={className}>
+      <div className='info'>
+        <AddressMini
+          className='origin'
+          value={origin}
+          withAddress={false}
+          isPadded={false}
+        />
+        <MessageSignature
+          message={message}
+          params={params}
+        />
+        <span className='date-time'>
+          {dateTime.toLocaleDateString()}
+          {' '}
+          {dateTime.toLocaleTimeString()}
+        </span>
+        <Button
+          className='icon-button clear-btn'
+          icon='close'
+          size='mini'
+          isPrimary
+          onClick={onClear}
+        />
+      </div>
+      <Output
+        isError={!success}
+        className='output'
+        value={output || '()'}
+        withCopy
+        withLabel={false}
+      />
+    </div>
+  );
+}
+
+export default styled(Outcome)`
+  & {
+    .info {
+      display: inline-flex;
+      justify-content: center;
+      align-items: center;
+      padding: 0.5rem;
+
+      & > *:not(:first-child) {
+        padding-left: 1.5rem !important;
+      }
+    }
+
+    .clear-btn {
+      display: none;
+    }
+
+    .date-time {
+      color: #aaa;
+    }
+
+    .origin {
+      padding-left: 0 !important;
+
+      * {
+        margin-left: 0 !important;
+      }
+    }
+
+    .output {
+      font-family: monospace;
+      margin-left: 3.5rem;
+
+      .ui--output {
+        border-color: #aaa;
+        margin: 0;
+      }
+    }
+
+    &:hover {
+      .clear-btn {
+        display: block;
+      }
+    }
+  }
+`;

+ 55 - 31
packages/app-contracts/src/Contracts/index.tsx

@@ -2,48 +2,70 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { CallContract, NullContract, I18nProps } from '@polkadot/react-components/types';
+import { ApiProps } from '@polkadot/react-api/types';
+import { I18nProps, StringOrNull } from '@polkadot/react-components/types';
 import { ComponentProps } from '../types';
 
-import React, { useState } from 'react';
-import { RouteComponentProps } from 'react-router';
-import { withRouter } from 'react-router-dom';
+import React, { useState, useEffect } from 'react';
+import { PromiseContract as ApiContract } from '@polkadot/api-contract';
+import { withApi, withMulti } from '@polkadot/react-api';
 import { Button, CardGrid } from '@polkadot/react-components';
 
 import translate from '../translate';
 import Add from './Add';
-import Contract from './Contract';
+import ContractCard from './Contract';
 import Call from './Call';
 import { getContractForAddress } from './util';
 
-interface Props extends ComponentProps, I18nProps, RouteComponentProps {}
+interface Props extends ComponentProps, I18nProps, ApiProps {}
+
+function filterContracts ({ api, accounts, contracts: keyringContracts }: Props): ApiContract[] {
+  return accounts && keyringContracts && Object.keys(keyringContracts)
+    .map((address): ApiContract | null => getContractForAddress(api, address))
+    .filter((contract: ApiContract | null): boolean => !!contract) as ApiContract[];
+}
 
 function Contracts (props: Props): React.ReactElement<Props> {
-  const { accounts, basePath, contracts, hasCode, showDeploy, t } = props;
-  // const { callAddress, callMethod, isAddOpen, isCallOpen } = this.state;
+  const { accounts, basePath, contracts: keyringContracts, hasCode, showDeploy, t } = props;
+  // const { callAddress, callMessage, isAddOpen, isCallOpen } = this.state;
 
-  const [callContract, setCallContract] = useState<CallContract | null>(null);
-  const [callMethodIndex, setCallMethodIndex] = useState<number | null>(null);
+  const [contracts, setContracts] = useState<ApiContract[]>(filterContracts(props));
+  const [callContractIndex, setCallContractIndex] = useState<number>(0);
+  const [callMessageIndex, setCallMessageIndex] = useState<number>(0);
   const [isAddOpen, setIsAddOpen] = useState(false);
   const [isCallOpen, setIsCallOpen] = useState(false);
 
+  useEffect((): void => {
+    setContracts(filterContracts(props));
+  }, [accounts, keyringContracts]);
+
+  let callContract = contracts[callContractIndex] || null;
+
+  useEffect((): void => {
+    callContract = contracts[callContractIndex];
+  }, [callContractIndex]);
+
   const _toggleAdd = (): void => setIsAddOpen(!isAddOpen);
   const _toggleCall = (): void => setIsCallOpen(!isCallOpen);
 
-  const _onChangeCallContract = (newCallContract: CallContract): void => {
-    if (callContract && newCallContract.address !== callContract.address) {
-      setCallMethodIndex(0);
+  const _onChangeCallContractAddress = (newCallContractAddress: StringOrNull): void => {
+    const index = contracts.findIndex(({ address }: ApiContract): boolean => newCallContractAddress === address.toString());
+
+    if (index > -1) {
+      index !== callContractIndex && setCallMessageIndex(0);
+      setCallContractIndex(index);
     }
-    setCallContract(callContract);
   };
-  const _onChangeCallMethodIndex = (callMethodIndex: number): void => {
-    !!callContract && setCallMethodIndex(callMethodIndex);
+
+  const _onChangeCallMessageIndex = (callMessageIndex: number): void => {
+    !!callContract && setCallMessageIndex(callMessageIndex);
   };
-  const _onCall = (callContract: CallContract): (_?: number) => () => void => {
-    return function (callMethodIndex?: number): () => void {
+
+  const _onCall = (callContractIndex: number): (_?: number) => () => void => {
+    return function (callMessageIndex?: number): () => void {
       return function (): void {
-        setCallContract(callContract);
-        setCallMethodIndex(callMethodIndex || 0);
+        setCallContractIndex(callContractIndex);
+        setCallMessageIndex(callMessageIndex || 0);
         setIsCallOpen(true);
       };
     };
@@ -75,16 +97,14 @@ function Contracts (props: Props): React.ReactElement<Props> {
           </Button.Group>
         }
       >
-        {(accounts && contracts && Object.keys(contracts)
-          .map((address): CallContract | NullContract => getContractForAddress(address))
-          .filter(({ abi, address }: CallContract | NullContract): boolean => !!address && !!abi) as CallContract[])
-          .map((contract: CallContract): React.ReactNode => {
+        {contracts
+          .map((contract: ApiContract, index): React.ReactNode => {
             return (
-              <Contract
+              <ContractCard
                 basePath={basePath}
                 contract={contract}
-                key={contract.address}
-                onCall={_onCall}
+                key={contract.address.toString()}
+                onCall={_onCall(index)}
               />
             );
           })}
@@ -96,14 +116,18 @@ function Contracts (props: Props): React.ReactElement<Props> {
       />
       <Call
         callContract={callContract}
-        callMethodIndex={callMethodIndex}
+        callMessageIndex={callMessageIndex}
         isOpen={isCallOpen}
-        onChangeCallContract={_onChangeCallContract}
-        onChangeCallMethodIndex={_onChangeCallMethodIndex}
+        onChangeCallContractAddress={_onChangeCallContractAddress}
+        onChangeCallMessageIndex={_onChangeCallMessageIndex}
         onClose={_toggleCall}
       />
     </>
   );
 }
 
-export default translate(withRouter(Contracts));
+export default withMulti(
+  Contracts,
+  translate,
+  withApi
+);

+ 16 - 21
packages/app-contracts/src/Contracts/util.tsx

@@ -2,52 +2,47 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { ContractABIFn, ContractABIMethod } from '@polkadot/api-contract/types';
-import { CallContract, NullContract, StringOrNull } from '@polkadot/react-components/types';
-import { CONTRACT_NULL } from '../constants';
+import { ContractABIFn, ContractABIMessage } from '@polkadot/api-contract/types';
+import { StringOrNull } from '@polkadot/react-components/types';
 
 import React from 'react';
+import { ApiPromise } from '@polkadot/api';
+import { PromiseContract as Contract } from '@polkadot/api-contract';
 import { MessageSignature } from '@polkadot/react-components';
 import { getContractAbi } from '@polkadot/react-components/util';
-import { stringCamelCase } from '@polkadot/util';
 
-export function findCallMethod (callContract: CallContract | null, callMethodIndex = 0): ContractABIMethod | null {
+export function findCallMethod (callContract: Contract | null, callMethodIndex = 0): ContractABIMessage | null {
   const message = callContract && callContract.abi.abi.contract.messages[callMethodIndex];
 
   return message || null;
 }
 
-export function getContractMethodFn (callContract: CallContract | null, callMethod: ContractABIMethod | null): ContractABIFn | null {
-  const fn = callContract && callContract.abi && callMethod && callContract.abi.messages[stringCamelCase(callMethod.name)];
+export function getContractMethodFn (callContract: Contract | null, callMethodIndex: number | null): ContractABIFn | null {
+  const fn = callContract && callContract.abi && callMethodIndex !== null && callContract.abi.messages[callMethodIndex];
 
   return fn || null;
 }
 
-export function getContractForAddress (address: StringOrNull): CallContract | NullContract {
+export function getContractForAddress (api: ApiPromise, address: StringOrNull): Contract | null {
   if (!address) {
-    return CONTRACT_NULL;
+    return null;
   } else {
     const abi = getContractAbi(address);
     return abi
-      ? {
-        address,
-        abi
-      }
-      : CONTRACT_NULL;
+      ? new Contract(api, abi, address)
+      : null;
   }
 }
 
-export function getCallMethodOptions (callContract: CallContract | null): any[] {
-  return callContract && callContract.abi
-    ? callContract.abi.abi.contract.messages.map((message, messageIndex): { key: string; text: React.ReactNode; value: string } => {
-      const key = message.name;
-
+export function getCallMessageOptions (callContract: Contract | null): any[] {
+  return callContract
+    ? callContract.messages.map(({ def: message, def: { name }, index }): { key: string; text: React.ReactNode; value: string } => {
       return {
-        key,
+        key: name,
         text: (
           <MessageSignature message={message} />
         ),
-        value: `${messageIndex}`
+        value: `${index}`
       };
     })
     : [];

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

@@ -31,7 +31,7 @@
   "homepage": "https://github.com/polkadot-js/ui/tree/master/packages/ui-reactive#readme",
   "dependencies": {
     "@babel/runtime": "^7.6.3",
-    "@polkadot/api": "^0.96.0-beta.16",
+    "@polkadot/api": "^0.96.0-beta.17",
     "@polkadot/extension-dapp": "^0.14.0-beta.1",
     "edgeware-node-types": "^1.0.10",
     "rxjs-compat": "^6.5.3"

+ 2 - 2
packages/react-components/src/AddressInfo.tsx

@@ -274,7 +274,7 @@ function renderBalances (props: Props): React.ReactNode {
             {stakingInfo.controllerId && (
               <TxButton
                 accountId={stakingInfo.controllerId.toString()}
-                className='iconButton'
+                className='icon-button'
                 icon='lock'
                 size='small'
                 isPrimary
@@ -375,7 +375,7 @@ export default withMulti(
           padding-right: 0 !important;
         }
 
-        button.ui.icon.primary.button.iconButton {
+        button.ui.icon.primary.button.icon-button {
           background: white !important;
         }
       }

+ 2 - 2
packages/react-components/src/CopyButton.tsx

@@ -41,7 +41,7 @@ function CopyButton ({ children, className, icon = 'copy', isAddress = false, t,
           {children}
           <span className='copySpan'>
             <Button
-              className='iconButton'
+              className='icon-button'
               icon={icon}
               size='mini'
               isPrimary
@@ -57,7 +57,7 @@ export default translate(
   styled(CopyButton)`
     cursor: copy;
 
-    button.ui.mini.icon.primary.button.iconButton {
+    button.ui.mini.icon.primary.button.icon-button {
       cursor: copy;
     }
 

+ 40 - 0
packages/react-components/src/IconLink.tsx

@@ -0,0 +1,40 @@
+// Copyright 2017-2019 @polkadot/react-components 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 { BareProps } from './types';
+
+import React from 'react';
+import styled from 'styled-components';
+
+import Icon from './Icon';
+
+interface Props extends BareProps {
+  icon?: string;
+  label?: React.ReactNode;
+  onClick: () => void;
+}
+
+function IconLink ({ className, icon, label, onClick }: Props): React.ReactElement<Props> {
+  return (
+    <a
+      className={className}
+      onClick={onClick}
+    >
+      {icon && <Icon className={icon} />}
+      {label}
+    </a>
+  );
+}
+
+export default styled(IconLink)`
+  font-size: 0.9rem !important;
+
+  &:hover {
+    text-decoration: underline;
+
+    i {
+      text-decoration: none;
+    }
+  }
+`;

+ 11 - 4
packages/react-components/src/MessageSignature.tsx

@@ -2,7 +2,7 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { ContractABIMethod } from '@polkadot/api-contract/types';
+import { ContractABIMessage } from '@polkadot/api-contract/types';
 import { I18nProps } from '@polkadot/react-components/types';
 
 import React from 'react';
@@ -15,7 +15,8 @@ import translate from './translate';
 
 export interface Props extends I18nProps {
   asConstructor?: boolean;
-  message: ContractABIMethod;
+  message: ContractABIMessage;
+  params?: any[];
   withTooltip?: boolean;
 }
 
@@ -43,7 +44,7 @@ const ReturnType = styled.span`
   color: #ff8600;
 `;
 
-function MessageSignature ({ message: { args, mutates, name, returnType }, asConstructor = false, withTooltip = false, t }: Props): React.ReactElement<Props> {
+function MessageSignature ({ message: { args, mutates, name, returnType }, params = [], asConstructor = false, withTooltip = false, t }: Props): React.ReactElement<Props> {
   return (
     <Signature>
       <Name>
@@ -56,7 +57,13 @@ function MessageSignature ({ message: { args, mutates, name, returnType }, asCon
             {name}:
             {' '}
             <Type>
-              {displayType(type)}
+              {params && params[index]
+                ? (
+                  <b>
+                    {params[index].toString()}
+                  </b>
+                )
+                : displayType(type)}
             </Type>
             {index < args.length - 1 && ', '}
           </React.Fragment>

+ 9 - 10
packages/react-components/src/Messages.tsx

@@ -2,7 +2,7 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { ContractABIMethod } from '@polkadot/api-contract/types';
+import { ContractABIMessage } from '@polkadot/api-contract/types';
 import { I18nProps } from '@polkadot/react-components/types';
 import { Button } from '@polkadot/react-components';
 
@@ -10,7 +10,7 @@ import React from 'react';
 import styled from 'styled-components';
 import { Abi } from '@polkadot/api-contract';
 
-import Icon from './Icon';
+import IconLink from './IconLink';
 import MessageSignature from './MessageSignature';
 import translate from './translate';
 import { classes } from './util';
@@ -52,7 +52,7 @@ function onSelectConstructor (props: Props, index: number): () => void {
   };
 }
 
-function renderItem (props: Props, message: ContractABIMethod, index: number, asConstructor = false): React.ReactNode {
+function renderItem (props: Props, message: ContractABIMessage, index: number, asConstructor = false): React.ReactNode {
   const { docs = [], name } = message;
 
   return (
@@ -124,19 +124,18 @@ function renderMessage (props: Props, index: number): React.ReactNode {
 }
 
 function Messages (props: Props): React.ReactElement<Props> {
-  const { className, contractAbi: { abi: { contract: { constructors, messages } } }, isLabelled, isRemovable, onRemove = NOOP, withConstructors } = props;
+  const { className, contractAbi: { abi: { contract: { constructors, messages } } }, isLabelled, isRemovable, onRemove = NOOP, withConstructors, t } = props;
   return (
     <div className={classes(className, 'ui--Messages', isLabelled && 'labelled')}>
       {withConstructors && constructors.map((_, index): React.ReactNode => renderConstructor(props, index))}
       {messages.map((_, index): React.ReactNode => renderMessage(props, index))}
       {isRemovable && (
-        <a
+        <IconLink
+          label={t('Remove ABI')}
+          icon='remove'
           className='remove-abi'
           onClick={onRemove}
-        >
-          <Icon name='remove' />
-          Remove ABI
-        </a>
+        />
       )}
     </div>
   );
@@ -150,7 +149,7 @@ export default translate(styled(Messages)`
   .remove-abi {
     float: right;
 
-    &:hover {
+    &:hover, &:hover :not(i) {
       text-decoration: underline;
     }
   }

+ 1 - 1
packages/react-components/src/Row.tsx

@@ -264,7 +264,7 @@ export default class Row<P extends RowProps, S extends RowState> extends React.P
     return (
       <span className='editSpan'>
         <Button
-          className='iconButton'
+          className='icon-button'
           icon='edit'
           size='mini'
           isPrimary

+ 1 - 0
packages/react-components/src/index.tsx

@@ -33,6 +33,7 @@ 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 IconLink } from './IconLink';
 export { default as IdentityIcon } from './IdentityIcon';
 export { default as InfoForInput } from './InfoForInput';
 export { default as Input } from './Input';

+ 5 - 2
packages/react-components/src/styles/components.ts

@@ -44,8 +44,11 @@ export default css`
     margin: 0;
   }
 
-  button.ui.icon.iconButton {
-    padding: 0em 0em .3em .3em !important;
+  button.ui.icon.icon-button {
+    padding-top: 0;
+    padding-right: 0;
+    padding-bottom: 0.3em;
+    padding-left: 0.3em;
     color: #2e86ab  !important;
     background: none  !important;
   }

+ 45 - 45
yarn.lock

@@ -2016,45 +2016,45 @@
     once "^1.4.0"
     universal-user-agent "^4.0.0"
 
-"@polkadot/api-contract@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-0.96.0-beta.16.tgz#0c46e6b69d4dd8b5f3327b1d0ef61db6ec27ceba"
-  integrity sha512-kfaypjOd/YjpUyzxZH6WGW8rtj6rbevpCIDmWQaqYg5c4RpC1JO0iFzx6Q4vRLRwnRGZorygKdY7rTrI/Yh3lw==
+"@polkadot/api-contract@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-0.96.0-beta.17.tgz#1f3465ff8fe4741833bb4489dd9ba68e63e81fa2"
+  integrity sha512-Y17MlyjmTP1OF42v96kj7ZUS+E9E2Op0z80u8M108Nss8YR28V7AHMh3X2X3PPEW91vk9vjRFxVquRRuVlNt7g==
   dependencies:
     "@babel/runtime" "^7.6.3"
-    "@polkadot/types" "^0.96.0-beta.16"
+    "@polkadot/types" "^0.96.0-beta.17"
 
-"@polkadot/api-derive@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.96.0-beta.16.tgz#2e8e22a18c5ea78d013ac3ddf6b6b598b35bdbcf"
-  integrity sha512-HdgynIq6EeggVpNWndZe9S+3UIKZGFUyQf+NDjsP0On9XI1E3Q+3KQRduRVjsgMCo4bQSTp9CTUT9Fdc0EbBjQ==
+"@polkadot/api-derive@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.96.0-beta.17.tgz#1ffcf15534c91adcadca88cb68f3e8ce6ac94535"
+  integrity sha512-QzO+26/Z4cSYwBYHsd7/Xq9b//XXR083sa7S9fOmBVeMOYj4rtXmmzzcl4k28byqFvUIT0Izt73m9aw8fSADKQ==
   dependencies:
     "@babel/runtime" "^7.6.3"
-    "@polkadot/api" "^0.96.0-beta.16"
-    "@polkadot/types" "^0.96.0-beta.16"
+    "@polkadot/api" "^0.96.0-beta.17"
+    "@polkadot/types" "^0.96.0-beta.17"
 
-"@polkadot/api-metadata@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/api-metadata/-/api-metadata-0.96.0-beta.16.tgz#6722617e0a26ce18d70533f26a72f3d7bc8d87d3"
-  integrity sha512-xNGoO0IgaNv4NkFWIsXHsbi5Hks55Alz6GcPH1rgeFAq8odGm+Dosbpg/N1ftPHV0v/hyy/e9bqJDpGYwUD8+A==
+"@polkadot/api-metadata@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/api-metadata/-/api-metadata-0.96.0-beta.17.tgz#4039943c18dbf7b143e48c2ee95365396e137b05"
+  integrity sha512-j9JKQuH114RcCSsE2X6NWfjPZgyLRn2plFnc6ZKB6c3rFbfDYwVtazAqaRi/IpOLnFRj+h1Rfapbu1ihz6q8nw==
   dependencies:
     "@babel/runtime" "^7.6.3"
-    "@polkadot/types" "^0.96.0-beta.16"
+    "@polkadot/types" "^0.96.0-beta.17"
     "@polkadot/util" "^1.6.1"
     "@polkadot/util-crypto" "^1.6.1"
 
-"@polkadot/api@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.96.0-beta.16.tgz#cf6a5b20a097743246fc098b25b9a4195a6e1982"
-  integrity sha512-qIEbARUbWEoSPnLJmLvDNbSctE9ilYKtn3QzBD4rnidvcLi+xFG9YvDvc0S8KojK5DtkQK6FaqW/Tt2E8gCUsg==
+"@polkadot/api@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.96.0-beta.17.tgz#ecff2ccd549d16f297683c69ab23fe0f84d1b318"
+  integrity sha512-E+0wSfUMuQrEotxfqeveqJaWoX+zXBMhHN/HesCdMFTEHcm5wlD7N+tfAY3yg3nzWQkDKlao728Y6fqsnFyKDQ==
   dependencies:
     "@babel/runtime" "^7.6.3"
-    "@polkadot/api-derive" "^0.96.0-beta.16"
-    "@polkadot/api-metadata" "^0.96.0-beta.16"
+    "@polkadot/api-derive" "^0.96.0-beta.17"
+    "@polkadot/api-metadata" "^0.96.0-beta.17"
     "@polkadot/keyring" "^1.6.1"
-    "@polkadot/rpc-core" "^0.96.0-beta.16"
-    "@polkadot/rpc-provider" "^0.96.0-beta.16"
-    "@polkadot/types" "^0.96.0-beta.16"
+    "@polkadot/rpc-core" "^0.96.0-beta.17"
+    "@polkadot/rpc-provider" "^0.96.0-beta.17"
+    "@polkadot/types" "^0.96.0-beta.17"
     "@polkadot/util-crypto" "^1.6.1"
 
 "@polkadot/dev-react@^0.32.0-beta.12":
@@ -2156,10 +2156,10 @@
   dependencies:
     "@babel/runtime" "^7.6.3"
 
-"@polkadot/jsonrpc@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.96.0-beta.16.tgz#b6593613aab8921f4b282d4dd2bd9b15be44b1f0"
-  integrity sha512-2L0wfsZfIJm8iSMT107d71uUmbHepkIK08PKmAXxZ36cak7Iy7nkq+oPA1I4vxghgthqdSypoikN/l8S4Cu3dw==
+"@polkadot/jsonrpc@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.96.0-beta.17.tgz#888249f017db65ddda294ef1a7277c67875771a0"
+  integrity sha512-cPqSjZrMEGYAwoxSyqth/P9EX7l8frWO1psH2AcK8zQs2/7PzGVJHoqMXDfBz541d5mm6egGkJJ1AvHc37Wxvg==
   dependencies:
     "@babel/runtime" "^7.6.3"
 
@@ -2197,25 +2197,25 @@
     qrcode-generator "^1.4.4"
     react-qr-reader "^2.2.1"
 
-"@polkadot/rpc-core@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.96.0-beta.16.tgz#3106dc3c3ed541f81096012221f8cf35a4d09603"
-  integrity sha512-El/XwN7hVoYDD3tJcuFEWJYYLKZGtBxGDbGvbSy7LmYFScsT5L96DuDIMepLrjy1XK1T7hAyvFR70Ed+PEx7Gg==
+"@polkadot/rpc-core@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.96.0-beta.17.tgz#474718a9f77661f0fecd46042666cefa6251f8cd"
+  integrity sha512-d50fmHsz62TB87k6pTUQKyNUFHp+k5Mts7VsmI0AxCWTyfwfeWVJM7TBqSIeHNcs6nW9BB1wwWvgoHUXLOOT6Q==
   dependencies:
     "@babel/runtime" "^7.6.3"
-    "@polkadot/jsonrpc" "^0.96.0-beta.16"
-    "@polkadot/rpc-provider" "^0.96.0-beta.16"
-    "@polkadot/types" "^0.96.0-beta.16"
+    "@polkadot/jsonrpc" "^0.96.0-beta.17"
+    "@polkadot/rpc-provider" "^0.96.0-beta.17"
+    "@polkadot/types" "^0.96.0-beta.17"
     "@polkadot/util" "^1.6.1"
     rxjs "^6.5.3"
 
-"@polkadot/rpc-provider@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.96.0-beta.16.tgz#884225d5b05bdd18551bba2fcc12224412b4f391"
-  integrity sha512-3B3aepAh5Nyb9bATqqKgyy4Y6Ujj8uRF4rmRlSAaijuMHJAEgrHx2yoHVPNQIyGZlRdPnG2Nr4zs/J+ewbO2zA==
+"@polkadot/rpc-provider@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.96.0-beta.17.tgz#3c3bf2b94f22993971e26b8c361c30b4deaf010e"
+  integrity sha512-zrIk4bJUbec9WCFu4Ry/qXoR9wUe2ggEcYSMDuvbCZZaKSpNzk2/qBBcAzPriLMUy0Kykb23xjnxoQehj+Wvag==
   dependencies:
     "@babel/runtime" "^7.6.3"
-    "@polkadot/api-metadata" "^0.96.0-beta.16"
+    "@polkadot/api-metadata" "^0.96.0-beta.17"
     "@polkadot/util" "^1.6.1"
     "@polkadot/util-crypto" "^1.6.1"
     "@types/nock" "^11.1.0"
@@ -2230,10 +2230,10 @@
   dependencies:
     "@types/chrome" "^0.0.91"
 
-"@polkadot/types@^0.96.0-beta.16":
-  version "0.96.0-beta.16"
-  resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.96.0-beta.16.tgz#eb843128c720ab284dc79c88c9f22b042b2c7d58"
-  integrity sha512-SahHunUp9q2HeYs0PIIGRUF/7I0KtG1CWp9Z4nj0kwEqs440oU+Dc/BqHc4lMR5OIU5plPdrzZEiEV6trI79Xw==
+"@polkadot/types@^0.96.0-beta.17":
+  version "0.96.0-beta.17"
+  resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.96.0-beta.17.tgz#5b5804d803d48f0774e89a9b14bb30226c045aca"
+  integrity sha512-9iV8H1bJbFbIyxDZLLbZ4buVG1weHksuvM8AzEWu9BxPgNGigUrxoRLaUC02qjmPKPfhwCQ4Q2XS8NfaoPiCcA==
   dependencies:
     "@babel/runtime" "^7.6.3"
     "@polkadot/util" "^1.6.1"