Contract call interface using RPC for non-mutating messages (#1745)

* start

* Droptdown functional

* continue

* initial working version
This commit is contained in:
kwingram25 2019-10-21 16:25:05 +03:00 committed by Jaco Greeff
parent 878f0f70e8
commit 1dc4e0e967
10 changed files with 592 additions and 569 deletions

View File

@ -3,311 +3,273 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { ApiProps } from '@polkadot/react-api/types';
import { BareProps, I18nProps } from '@polkadot/react-components/types';
import { BareProps, CallContract, I18nProps, StringOrNull } from '@polkadot/react-components/types';
import { QueueProps } from '@polkadot/react-components/Status/types';
import { ContractExecResult } from '@polkadot/types/interfaces/contracts';
import BN from 'bn.js';
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { Abi } from '@polkadot/api-contract';
import { Button, Dropdown, InputAddress, InputBalance, InputNumber, Modal, TxButton, TxComponent } from '@polkadot/react-components';
import { getContractAbi } from '@polkadot/react-components/util';
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 { withApi, withMulti } from '@polkadot/react-api';
import { displayType } from '@polkadot/types';
import { isNull, isUndefined } from '@polkadot/util';
import Params from '../Params';
import translate from '../translate';
import Params from '../Params';
import { GAS_LIMIT } from '../constants';
import { findCallMethod, getContractForAddress, getCallMethodOptions, getContractMethodFn } from './util';
interface Props extends BareProps, I18nProps, ApiProps, RouteComponentProps<{}> {
address: string | null;
interface Props extends BareProps, I18nProps, ApiProps {
callContract: CallContract | null;
callMethodIndex: number | null;
isOpen: boolean;
method: string | null;
onChangeCallContract: (callContract: CallContract) => void;
onChangeCallMethodIndex: (callMethodIndex: number) => void;
onClose: () => void;
}
interface State {
accountId: string | null;
address: string | null;
contractAbi?: Abi | null;
endowment: BN;
gasLimit: BN;
isAddressValid: boolean;
isBusy: boolean;
method: string | null;
params: any[];
}
function Call (props: Props): React.ReactElement<Props> | null {
const { isOpen, callContract, callMethodIndex, onChangeCallContract, onChangeCallMethodIndex, onClose, api, t } = props;
class Call extends TxComponent<Props, State> {
public defaultState: State = {
address: null,
accountId: null,
endowment: new BN(0),
gasLimit: new BN(GAS_LIMIT),
method: null,
isAddressValid: false,
isBusy: false,
params: []
if (isNull(callContract) || isNull(callMethodIndex)) {
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;
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 _onChangeAccountId = (accountId: StringOrNull): void => setAccountId(accountId);
const _onChangeCallAddress = (callAddress: StringOrNull): void => {
const callContract = getContractForAddress(callAddress);
onChangeCallContract && callContract.abi && onChangeCallContract(callContract);
};
public state: State = this.defaultState;
const _onChangeCallMethodString = (callMethodString: string): void => {
setParams([]);
onChangeCallMethodIndex && onChangeCallMethodIndex(parseInt(callMethodString, 10) || 0);
};
public static getDerivedStateFromProps ({ address: propsAddress, method: propsMethod, isOpen }: Props, { address, method }: State): Pick<State, never> | null {
if (!isOpen) {
return {
address: null,
method: null,
contractAbi: null,
isAddressValid: false
};
}
const _onChangeEndowment = (endowment?: BN): void => endowment && setEndowment(endowment);
const _onChangeGasLimit = (gasLimit?: BN): void => gasLimit && setGasLimit(gasLimit);
return {
...(
!address
? {
address: propsAddress,
contractAbi: propsAddress ? getContractAbi(propsAddress) : null,
isAddressValid: !!propsAddress
}
: {}
),
...(
!method
? { method: propsMethod }
: {}
)
};
}
const _onChangeParams = (params: any[]): void => setParams(params);
const _toggleBusy = (): void => setIsBusy(!isBusy);
public render (): React.ReactNode {
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>
);
}
public renderContent (): React.ReactNode {
const { t } = this.props;
const { gasLimit } = this.state;
const [address, contractAbi, method] = this.getCallProps();
const isEndowValid = true;
const isGasValid = gasLimit.gtn(0);
if (!address || !contractAbi) {
return null;
}
const methodOptions = contractAbi
? Object.keys(contractAbi.messages).map((key): { key: string; text: string; value: string } => {
const fn = contractAbi.messages[key];
const type = fn.type ? `: ${displayType(fn.type)}` : '';
const args = fn.args.map(({ name, type }): string => `${name}: ${displayType(type)}`);
const text = `${key}(${args.join(', ')})${type}`;
return {
key,
text,
value: key
};
})
: [];
return (
<div className='contracts--Call'>
<InputAddress
help={t('Specify the user account to use for this contract call. And fees will be deducted from this account.')}
label={t('call from account')}
onChange={this.onChangeAccount}
type='account'
/>
<InputAddress
help={t('A deployed contract that has either been deployed or attached. The address and ABI are used to construct the parameters.')}
label={t('contract to use')}
onChange={this.onChangeAddress}
type='contract'
value={address}
/>
<Dropdown
help={t('The message to send to this contract. Parameters are adjusted based on the ABI provided.')}
isError={!method}
label={t('message to send')}
onChange={this.onChangeMethod}
options={methodOptions}
style={{ fontFamily: 'monospace' }}
value={method}
/>
<Params
onChange={this.onChangeParams}
onEnter={this.sendTx}
params={
method && contractAbi && contractAbi.messages[method]
? contractAbi.messages[method].args
: undefined
}
/>
<InputBalance
help={t('The allotted value for this contract, i.e. the amount transferred to the contract as part of this call.')}
isError={!isEndowValid}
label={t('value')}
onChange={this.onChangeEndowment}
/>
<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}
value={gasLimit}
onEnter={this.sendTx}
/>
</div>
);
}
private renderButtons (): React.ReactNode {
const { api, 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
icon='cancel'
isNegative
onClick={this.onClose}
label={t('Cancel')}
/>
<Button.Or />
<TxButton
accountId={accountId}
icon='sign-in'
isDisabled={!isValid}
isPrimary
label={t('Call')}
onClick={this.toggleBusy}
onFailed={this.toggleBusy}
onSuccess={this.toggleBusy}
params={this.constructCall}
tx={api.tx.contracts ? 'contracts.call' : 'contract.call'}
ref={this.button}
/>
</Button.Group>
);
}
private getCallProps = (): [string | null, Abi | 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 = (): any[] => {
const {
endowment, gasLimit, params
} = this.state;
const [address, contractAbi, method] = this.getCallProps();
if (!contractAbi || !method) {
const _constructTx = (): any[] => {
const fn = getContractMethodFn(callContract, callMethod);
if (!fn || !callContract || !callContract.address) {
return [];
}
return [address, endowment, gasLimit, contractAbi.messages[method](...params)];
}
return [callContract.address, endowment, gasLimit, fn(...params)];
};
private onChangeAccount = (accountId: string | null): void => {
this.setState({ accountId });
}
private onChangeAddress = (address: string | null): void => {
const contractAbi = getContractAbi(address);
this.setState({ address, contractAbi, isAddressValid: !!contractAbi });
}
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(GAS_LIMIT) });
}
private onChangeMethod = (method: string | null): void => {
this.setState({ method, params: [] });
}
private onChangeParams = (params: any[]): void => {
this.setState({ params });
}
private toggleBusy = (): void => {
this.setState(({ isBusy }): Pick<State, never> => ({
isBusy: !isBusy
}));
}
private reset = (): void => {
this.setState((state: State): Pick<State, never> => {
if (!state.isBusy) {
return {
...state,
...this.defaultState
};
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)
}
];
};
return {};
});
}
const isEndowmentValid = true;
const isGasValid = !gasLimit.isZero();
const isValid = !!accountId && isEndowmentValid && isGasValid && callContract && callContract.address && callContract.abi;
private onClose = (): void => {
const { onClose } = this.props;
return (
<Modal
className='app--contracts-Modal'
dimmer='inverted'
onClose={onClose}
open={isOpen}
>
<Modal.Header>
{t('Call a contract')}
</Modal.Header>
<Modal.Content>
{callContract && (
<div className='contracts--CallControls'>
<InputAddress
defaultValue={accountId}
help={t('Specify the user account to use for this contract call. And fees will be deducted from this account.')}
isDisabled={isBusy}
label={t('call from account')}
onChange={_onChangeAccountId}
type='account'
value={accountId}
/>
<InputAddress
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}
type='contract'
value={callContract.address}
/>
{callMethodIndex !== null && (
<>
<Dropdown
help={t('The message to send to this contract. Parameters are adjusted based on the ABI provided.')}
isDisabled={isBusy}
isError={callMethod === null}
label={t('message to send')}
onChange={_onChangeCallMethodString}
options={getCallMethodOptions(callContract)}
value={`${callMethodIndex}`}
/>
<Params
isDisabled={isBusy}
onChange={_onChangeParams}
params={
callMethod
? callMethod.args
: undefined
}
/>
</>
)}
<InputBalance
help={t('The allotted value for this contract, i.e. the amount transferred to the contract as part of this call.')}
isDisabled={isBusy}
isError={!isEndowmentValid}
label={t('value')}
onChange={_onChangeEndowment}
value={endowment}
/>
<InputNumber
defaultValue={gasLimit}
help={t('The maximum amount of gas that can be used by this call. If the code requires more, the call will fail.')}
isDisabled={isBusy}
isError={!isGasValid}
label={t('maximum gas allowed')}
onChange={_onChangeGasLimit}
value={gasLimit}
/>
</div>
)}
<QueueConsumer>
{
({ queueRpc, txqueue }: QueueProps): React.ReactNode => {
const _onSubmitRpc = (): void => {
const values = _constructRpc();
this.reset();
onClose && onClose();
}
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>
</>
)}
</>
);
}
}
</QueueConsumer>
</Modal.Content>
</Modal>
);
}
export default withMulti(
translate(withRouter(Call)),
Call,
translate,
withApi
);

View File

@ -3,28 +3,21 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { ActionStatus } from '@polkadot/react-components/Status/types';
import { I18nProps } from '@polkadot/react-components/types';
import { CallContract, I18nProps } from '@polkadot/react-components/types';
import React from 'react';
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 { AddressRow, Button, Card, Forget, Messages } from '@polkadot/react-components';
import { getContractAbi } from '@polkadot/react-components/util';
import translate from '../translate';
interface Props extends I18nProps, RouteComponentProps {
basePath: string;
address: string;
onCall: (callAddress?: string, callMethod?: string) => void;
}
interface State {
isBackupOpen: boolean;
isForgetOpen: boolean;
isPasswordOpen: boolean;
contract: CallContract;
onCall: (_: CallContract) => (_?: number) => () => void;
}
const ContractCard = styled(Card)`
@ -34,85 +27,17 @@ const ContractCard = styled(Card)`
}
`;
class Contract extends React.PureComponent<Props, State> {
public state: State = {
isBackupOpen: false,
isForgetOpen: false,
isPasswordOpen: false
};
function Contract (props: Props): React.ReactElement<Props> | null {
const { contract, contract: { abi, address }, onCall, t } = props;
public render (): React.ReactNode {
const { address, onCall, t } = this.props;
const contractAbi = getContractAbi(address);
if (!contractAbi) {
return null;
}
return (
<ContractCard>
{this.renderModals()}
<AddressRow
buttons={this.renderButtons()}
isContract
isEditable
type='contract'
value={address}
withBalance={false}
withNonce={false}
withTags
>
<details>
<summary>{t('Messages')}</summary>
<Messages
address={address}
contractAbi={contractAbi}
isRemovable={false}
onSelect={onCall}
/>
</details>
</AddressRow>
</ContractCard>
);
if (!address || !abi) {
return null;
}
private renderModals (): React.ReactNode {
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;
const [isForgetOpen, setIsForgetOpen] = useState(false);
const _toggleForget = (): void => setIsForgetOpen(!isForgetOpen);
const _onForget = (): void => {
if (!address) {
return;
}
@ -130,32 +55,65 @@ class Contract extends React.PureComponent<Props, State> {
status.status = 'error';
status.message = error.message;
}
this.toggleForget();
}
_toggleForget();
};
private renderButtons (): React.ReactNode {
const { address, onCall, t } = this.props;
const _onCallMessage = onCall(contract);
const _onCall = _onCallMessage();
return (
<div className='contracts--Contract-buttons'>
<Button
icon='trash'
isNegative
onClick={this.toggleForget}
size='small'
tooltip={t('Forget this contract')}
/>
<Button
icon='play'
isPrimary
label={t('execute')}
onClick={(): void => onCall(address)}
size='small'
tooltip={t('Call a method on this contract')}
/>
</div>
);
}
return (
<ContractCard>
{
isForgetOpen && (
<Forget
address={address}
mode='contract'
onForget={_onForget}
key='modal-forget-contract'
onClose={_toggleForget}
/>
)
}
<AddressRow
buttons={
<div className='contracts--Contract-buttons'>
<Button
icon='trash'
isNegative
onClick={_toggleForget}
size='small'
tooltip={t('Forget this contract')}
/>
<Button
icon='play'
isPrimary
label={t('execute')}
onClick={_onCall}
size='small'
tooltip={t('Call a method on this contract')}
/>
</div>
}
isContract
isEditable
type='contract'
value={address}
withBalance={false}
withNonce={false}
withTags
>
<details>
<summary>{t('Messages')}</summary>
<Messages
address={address}
contractAbi={abi}
isRemovable={false}
onSelect={_onCallMessage}
/>
</details>
</AddressRow>
</ContractCard>
);
}
export default translate(withRouter(Contract));

View File

@ -2,120 +2,108 @@
// 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/react-components/types';
import { CallContract, NullContract, I18nProps } from '@polkadot/react-components/types';
import { ComponentProps } from '../types';
import React from 'react';
import React, { useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { Button, CardGrid } from '@polkadot/react-components';
import { stringCamelCase } from '@polkadot/util';
import translate from '../translate';
import Add from './Add';
import Contract from './Contract';
import Call from './Call';
import { getContractForAddress } from './util';
interface Props extends ComponentProps, I18nProps, RouteComponentProps {}
interface State {
isAddOpen: boolean;
isCallOpen: boolean;
callAddress: string | null;
callMethod: string | null;
}
function Contracts (props: Props): React.ReactElement<Props> {
const { accounts, basePath, contracts, hasCode, showDeploy, t } = props;
// const { callAddress, callMethod, isAddOpen, isCallOpen } = this.state;
class Contracts extends React.PureComponent<Props, State> {
public state: State = {
callAddress: null,
callMethod: null,
isAddOpen: false,
isCallOpen: false
const [callContract, setCallContract] = useState<CallContract | null>(null);
const [callMethodIndex, setCallMethodIndex] = useState<number | null>(null);
const [isAddOpen, setIsAddOpen] = useState(false);
const [isCallOpen, setIsCallOpen] = useState(false);
const _toggleAdd = (): void => setIsAddOpen(!isAddOpen);
const _toggleCall = (): void => setIsCallOpen(!isCallOpen);
const _onChangeCallContract = (newCallContract: CallContract): void => {
if (callContract && newCallContract.address !== callContract.address) {
setCallMethodIndex(0);
}
setCallContract(callContract);
};
const _onChangeCallMethodIndex = (callMethodIndex: number): void => {
!!callContract && setCallMethodIndex(callMethodIndex);
};
const _onCall = (callContract: CallContract): (_?: number) => () => void => {
return function (callMethodIndex?: number): () => void {
return function (): void {
setCallContract(callContract);
setCallMethodIndex(callMethodIndex || 0);
setIsCallOpen(true);
};
};
};
public render (): React.ReactNode {
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
icon='cloud upload'
isPrimary
label={t('Deploy a code hash')}
onClick={showDeploy()}
/>
<Button.Or />
</>
)}
<Button
icon='add'
isPrimary
label={t('Add an existing contract')}
onClick={this.showAdd}
/>
</Button.Group>
}
>
{accounts && contracts && Object.keys(contracts).map((address): React.ReactNode => {
return (
<>
<CardGrid
emptyText={t('No contracts available')}
buttons={
<Button.Group>
{hasCode && (
<>
<Button
icon='cloud upload'
isPrimary
label={t('Deploy a code hash')}
onClick={showDeploy()}
/>
<Button.Or />
</>
)}
<Button
icon='add'
isPrimary
label={t('Add an existing contract')}
onClick={_toggleAdd}
/>
</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 => {
return (
<Contract
basePath={basePath}
address={address}
key={address}
onCall={this.showCall}
contract={contract}
key={contract.address}
onCall={_onCall}
/>
);
})}
</CardGrid>
<Add
basePath={basePath}
isOpen={isAddOpen}
onClose={this.hideAdd}
/>
<Call
address={callAddress}
isOpen={isCallOpen}
method={callMethod}
onClose={this.hideCall}
/>
</>
);
}
private showAdd = (): void => {
this.setState({
isAddOpen: true
});
}
private hideAdd = (): void => {
this.setState({
isAddOpen: false
});
}
private showCall = (callAddress?: string, callMethod?: string): void => {
this.setState({
isCallOpen: true,
callAddress: callAddress || null,
callMethod: callMethod ? stringCamelCase(callMethod) : null
});
}
private hideCall = (): void => {
this.setState({
isCallOpen: false,
callAddress: null,
callMethod: null
});
}
</CardGrid>
<Add
basePath={basePath}
isOpen={isAddOpen}
onClose={_toggleAdd}
/>
<Call
callContract={callContract}
callMethodIndex={callMethodIndex}
isOpen={isCallOpen}
onChangeCallContract={_onChangeCallContract}
onChangeCallMethodIndex={_onChangeCallMethodIndex}
onClose={_toggleCall}
/>
</>
);
}
export default translate(withRouter(Contracts));

View File

@ -0,0 +1,54 @@
// 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 { ContractABIFn, ContractABIMethod } from '@polkadot/api-contract/types';
import { CallContract, NullContract, StringOrNull } from '@polkadot/react-components/types';
import { CONTRACT_NULL } from '../constants';
import React from 'react';
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 {
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)];
return fn || null;
}
export function getContractForAddress (address: StringOrNull): CallContract | NullContract {
if (!address) {
return CONTRACT_NULL;
} else {
const abi = getContractAbi(address);
return abi
? {
address,
abi
}
: CONTRACT_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;
return {
key,
text: (
<MessageSignature message={message} />
),
value: `${messageIndex}`
};
})
: [];
}

View File

@ -14,17 +14,16 @@ import { SubmittableResult } from '@polkadot/api';
import { Abi } from '@polkadot/api-contract';
import { withApi, withMulti } from '@polkadot/react-api';
import keyring from '@polkadot/ui-keyring';
import { Button, Dropdown, InputBalance, TxButton } from '@polkadot/react-components';
import { Button, Dropdown, InputBalance, MessageSignature, TxButton } from '@polkadot/react-components';
import createValues from '@polkadot/react-params/values';
import { displayType } from '@polkadot/types';
import ContractModal, { ContractModalProps, ContractModalState } from './Modal';
import Params from './Params';
import store from './store';
import translate from './translate';
import { GAS_LIMIT, ENDOWMENT } from './constants';
import { GAS_LIMIT } from './constants';
type ConstructOptions = { key: string; text: string; value: string }[];
type ConstructOptions = { key: string; text: React.ReactNode; value: string }[];
interface Props extends ContractModalProps, ApiProps, I18nProps, RouteComponentProps {
codeHash?: string;
@ -52,7 +51,7 @@ class Deploy extends ContractModal<Props, State> {
...this.defaultState,
constructorIndex: -1,
constructOptions: [],
endowment: new BN(ENDOWMENT),
endowment: new BN(0),
gasLimit: new BN(GAS_LIMIT),
isHashValid: false,
params: [],
@ -126,12 +125,14 @@ class Deploy extends ContractModal<Props, State> {
const constructor = constructors[constructorIndex];
const constructOptions: ConstructOptions = constructors.map(
(constr) => {
const { name, args } = constr;
const textArgs = args.map(({ name, type }): string => `${name}: ${displayType(type)}`);
return {
key: `${constructorIndex}`,
text: `${name}(${textArgs.join(', ')})`,
text: (
<MessageSignature
asConstructor
message={constr}
/>
),
value: `${constructorIndex}`
};
});
@ -147,7 +148,6 @@ class Deploy extends ContractModal<Props, State> {
const { t } = this.props;
const { codeHash, constructorIndex, constructOptions, contractAbi, endowment, isAbiSupplied, isBusy, isHashValid } = this.state;
const isEndowValid = !endowment.isZero();
const codeOptions = store.getAllCode().map(({ json: { codeHash, name } }): { text: string; value: string } => ({
text: `${name} (${codeHash})`,
value: codeHash
@ -182,11 +182,12 @@ class Deploy extends ContractModal<Props, State> {
<Dropdown
help={t('The deployment constructor information for this contract, as provided by the ABI.')}
isDisabled={contractAbi.abi.contract.constructors.length <= 1}
label={t('constructor')}
label={t('constructor ')}
onChange={this.onChangeConstructorIndex}
options={constructOptions}
style={{ fontFamily: 'monospace' }}
value={`${constructorIndex}`}
withLabel
/>
)
: null
@ -204,7 +205,7 @@ class Deploy extends ContractModal<Props, State> {
<InputBalance
help={t('The allotted endowment for this contract, i.e. the amount transferred to the contract upon instantiation.')}
isDisabled={isBusy}
isError={!isEndowValid}
isError={endowment.isZero()}
label={t('endowment')}
onChange={this.onChangeEndowment}
onEnter={this.sendTx}

View File

@ -4,3 +4,7 @@
export const ENDOWMENT = 1000;
export const GAS_LIMIT = 500000;
export const CONTRACT_NULL = {
abi: null,
address: null
};

View File

@ -0,0 +1,95 @@
// 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 { ContractABIMethod } from '@polkadot/api-contract/types';
import { I18nProps } from '@polkadot/react-components/types';
import React from 'react';
import styled from 'styled-components';
import { displayType } from '@polkadot/types';
import Icon from './Icon';
import Tooltip from './Tooltip';
import translate from './translate';
export interface Props extends I18nProps {
asConstructor?: boolean;
message: ContractABIMethod;
withTooltip?: boolean;
}
const Signature = styled.div`
font-family: monospace;
font-weight: normal;
.mutates {
color: #ff8600;
margin-left: 0.5rem;
opacity: 0.6;
}
`;
const Name = styled.span`
color: #2f8ddb;
font-weight: bold;
`;
const Type = styled.span`
color: #21a2b2;
`;
const ReturnType = styled.span`
color: #ff8600;
`;
function MessageSignature ({ message: { args, mutates, name, returnType }, asConstructor = false, withTooltip = false, t }: Props): React.ReactElement<Props> {
return (
<Signature>
<Name>
{name}
</Name>
(
{args.map(({ name, type }, index): React.ReactNode => {
return (
<React.Fragment key={`${name}-args-${index}`}>
{name}:
{' '}
<Type>
{displayType(type)}
</Type>
{index < args.length - 1 && ', '}
</React.Fragment>
);
})}
)
{(!asConstructor && returnType) && (
<>
:
{' '}
<ReturnType>
{displayType(returnType)}
</ReturnType>
</>
)}
{mutates && (
<>
<Icon
data-tip
data-for={`mutates-${name}`}
name="database"
className="mutates"
/>
{withTooltip && (
<Tooltip
trigger={`mutates-${name}`}
text={t('Mutates contract state')}
/>
)}
</>
)}
</Signature>
);
}
export default translate(MessageSignature);

View File

@ -9,10 +9,9 @@ import { Button } from '@polkadot/react-components';
import React from 'react';
import styled from 'styled-components';
import { Abi } from '@polkadot/api-contract';
import { displayType } from '@polkadot/types';
import Icon from './Icon';
import Tooltip from './Tooltip';
import MessageSignature from './MessageSignature';
import translate from './translate';
import { classes } from './util';
@ -22,24 +21,22 @@ export interface Props extends I18nProps {
isLabelled?: boolean;
isRemovable: boolean;
onRemove?: () => void;
onSelect?: (callAddress?: string, callMethod?: string) => void;
onSelectConstructor?: (index: number) => void;
onSelect?: (messageIndex: number) => () => void;
onSelectConstructor?: (constructorIndex: number) => void;
withConstructors?: boolean;
}
const NOOP = (): void => {};
function onSelect (props: Props, index: number): () => void {
function onSelect (props: Props, messageIndex: number): () => void {
return function (): void {
const { address: callAddress, contractAbi: { abi: { contract: { messages } } }, onSelect } = props;
if (!callAddress || !messages || !messages[index]) {
if (!callAddress || !messages || !messages[messageIndex]) {
return;
}
const { name } = messages[index];
onSelect && onSelect(callAddress, name);
onSelect && onSelect(messageIndex)();
};
}
@ -50,60 +47,25 @@ function onSelectConstructor (props: Props, index: number): () => void {
if (!constructors || !constructors[index]) {
return;
}
console.log(onSelectConstructor);
onSelectConstructor && onSelectConstructor(index);
};
}
function renderItem (props: Props, { args, docs = [], mutates, name, returnType }: ContractABIMethod, index: number, asConstructor = false): React.ReactNode {
function renderItem (props: Props, message: ContractABIMethod, index: number, asConstructor = false): React.ReactNode {
const { docs = [], name } = message;
return (
<div
key={name}
className={classes('message', !onSelect && 'exempt-hover', asConstructor && 'constructor')}
>
<div className="info">
<div className="signature">
<span className="name">
{name}
</span>
(
{args.map(({ name, type }, index): React.ReactNode => {
return (
<React.Fragment key={`${name}-args-${index}`}>
{name}:
{' '}
<span className='type'>
{displayType(type)}
</span>
{index < args.length - 1 && ', '}
</React.Fragment>
);
})}
)
{(!asConstructor && returnType) && (
<>
:
{' '}
<span className="return-type">
{displayType(returnType)}
</span>
</>
)}
{mutates && (
<>
<Icon
data-tip
data-for={`mutates-${name}`}
name="database"
className="mutates"
/>
<Tooltip
trigger={`mutates-${name}`}
text={props.t('Mutates contract state')}
/>
</>
)}
</div>
<MessageSignature
asConstructor={asConstructor}
message={message}
withTooltip
/>
<details className="docs">
<summary>
{
@ -229,34 +191,10 @@ export default translate(styled(Messages)`
.info {
flex: 1 1;
.signature {
font-family: monospace;
font-weight: normal;
.name {
color: #2f8ddb;
font-weight: bold;
}
.type {
color: #21a2b2;
}
.return-type {
color: #ff8600;
}
}
.docs {
font-size: 0.8rem;
font-weight: normal;
}
.mutates {
color: #ff8600;
margin-left: 0.5rem;
opacity: 0.6;
}
}
.accessory {

View File

@ -54,6 +54,7 @@ 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 MessageSignature } from './MessageSignature';
export { default as Modal } from './Modal';
export { default as Nonce } from './Nonce';
export { default as Output } from './Output';

View File

@ -3,6 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { WithTranslation } from 'react-i18next';
import { Abi } from '@polkadot/api-contract';
import { ActionStatus } from '@polkadot/react-components/Status/types';
export interface BareProps {
@ -18,3 +19,24 @@ export interface AppProps {
export type I18nProps = BareProps & WithTranslation;
export type BitLength = 8 | 16 | 32 | 64 | 128 | 256;
export type StringOrNull = string | null;
interface ContractBase {
abi: Abi;
}
export interface Contract extends ContractBase {
address: null;
}
export interface ContractDeployed extends ContractBase {
address: string;
}
export type CallContract = ContractDeployed;
export interface NullContract {
abi: null;
address: null;
}