Watch contract state (#3838)

* Watch contract state

* Display actual outputs
This commit is contained in:
Jaco Greeff 2020-10-10 11:27:23 +02:00 committed by GitHub
parent e28181cdb1
commit 6bdb12a2ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 237 additions and 200 deletions

View File

@ -19,8 +19,7 @@ function Codes ({ onShowDeploy }: Props): React.ReactElement<Props> {
const headerRef = useRef<[string?, string?, number?][]>([
[t('code hashes'), 'start'],
[],
[]
[undefined, undefined, 2]
]);
return (

View File

@ -5,8 +5,8 @@ import { ActionStatus } from '@polkadot/react-components/Status/types';
import React, { useCallback } from 'react';
import keyring from '@polkadot/ui-keyring';
import { PromiseContract as ApiContract } from '@polkadot/api-contract';
import { AddressMini, Button, Forget } from '@polkadot/react-components';
import { PromiseContract } from '@polkadot/api-contract';
import { AddressInfo, AddressMini, Button, Forget } from '@polkadot/react-components';
import { useToggle } from '@polkadot/react-hooks';
import Messages from '../shared/Messages';
@ -14,27 +14,23 @@ import { useTranslation } from '../translate';
interface Props {
className?: string;
contract: ApiContract;
onCall: (_?: number) => () => void;
contract: PromiseContract;
onCall: (index?: number) => void;
}
function Contract ({ className, contract: { abi, address }, onCall }: Props): React.ReactElement<Props> | null {
function Contract ({ className, contract, onCall }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const [isForgetOpen, toggleIsForgetOpen] = useToggle();
const _onForget = useCallback(
(): void => {
if (!address) {
return;
}
const status: Partial<ActionStatus> = {
account: address,
account: contract.address,
action: 'forget'
};
try {
keyring.forgetContract(address.toString());
keyring.forgetContract(contract.address.toString());
status.status = 'success';
status.message = t<string>('address forgotten');
} catch (error) {
@ -44,36 +40,41 @@ function Contract ({ className, contract: { abi, address }, onCall }: Props): Re
toggleIsForgetOpen();
},
[address, t, toggleIsForgetOpen]
[contract.address, t, toggleIsForgetOpen]
);
if (!address || !abi) {
return null;
}
return (
<tr className={className}>
<td className='address top'>
{isForgetOpen && (
<Forget
address={address.toString()}
address={contract.address.toString()}
key='modal-forget-contract'
mode='contract'
onClose={toggleIsForgetOpen}
onForget={_onForget}
/>
)}
<AddressMini value={address} />
<AddressMini value={contract.address} />
</td>
<td className='all top'>
<Messages
address={address.toString()}
contractAbi={abi}
isRemovable={false}
address={contract.address.toString()}
contract={contract}
contractAbi={contract.abi}
isWatching
onSelect={onCall}
withMessages
/>
</td>
<td className='number'>
<AddressInfo
address={contract.address}
withBalance
withBalanceToggle
withExtended={false}
/>
</td>
<td className='button'>
<Button
icon='trash'
@ -82,7 +83,7 @@ function Contract ({ className, contract: { abi, address }, onCall }: Props): Re
<Button
icon='play'
label={t<string>('exec')}
onClick={onCall()}
onClick={onCall}
/>
</td>
</tr>

View File

@ -10,7 +10,7 @@ import { Table } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
import ContractCard from './Contract';
import Contract from './Contract';
import Call from './Call';
import { getContractForAddress } from './util';
@ -34,8 +34,7 @@ function Contracts ({ contracts: keyringContracts }: Props): React.ReactElement<
const headerRef = useRef<[string?, string?, number?][]>([
[t('contracts'), 'start'],
[],
[]
[undefined, undefined, 3]
]);
const contracts = useMemo(
@ -68,12 +67,11 @@ function Contracts ({ contracts: keyringContracts }: Props): React.ReactElement<
const _onCall = useCallback(
(callContractIndex: number) =>
(callMessageIndex?: number) =>
(): void => {
setCallContractIndex(callContractIndex);
setCallMessageIndex(callMessageIndex || 0);
setIsCallOpen(true);
},
(callMessageIndex?: number): void => {
setCallContractIndex(callContractIndex);
setCallMessageIndex(callMessageIndex || 0);
setIsCallOpen(true);
},
[]
);
@ -84,7 +82,7 @@ function Contracts ({ contracts: keyringContracts }: Props): React.ReactElement<
header={headerRef.current}
>
{contracts.map((contract: ApiContract, index): React.ReactNode => (
<ContractCard
<Contract
contract={contract}
key={contract.address.toString()}
onCall={_onCall(index)}

View File

@ -34,7 +34,6 @@ function renderMessages ({ contractAbi, onRemove, onSelectConstructor, withConst
<Messages
contractAbi={contractAbi}
isLabelled={withLabel}
isRemovable={false}
onRemove={onRemove}
onSelectConstructor={onSelectConstructor}
withConstructors={withConstructors}

View File

@ -0,0 +1,132 @@
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { AbiConstructor, AbiMessage, ContractCallOutcome } from '@polkadot/api-contract/types';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { Button, Output } from '@polkadot/react-components';
import valueToText from '@polkadot/react-params/valueToText';
import MessageSignature from './MessageSignature';
import { useTranslation } from '../translate';
export interface Props {
className?: string;
index: number;
lastResult?: ContractCallOutcome;
message: AbiConstructor | AbiMessage;
onSelect?: (index: number) => void;
}
function filterDocs (docs: string[]): string[] {
let skip = false;
return docs
.map((line) => line.trim())
.filter((line) => line)
.filter((line, index): boolean => {
if (skip) {
return false;
} else if (index || line.startsWith('#')) {
skip = true;
return false;
}
return true;
});
}
function Message ({ className = '', index, lastResult, message, onSelect }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const _onSelect = useCallback(
() => onSelect && onSelect(index),
[index, onSelect]
);
console.log(lastResult, lastResult?.output, lastResult?.output?.toHuman());
return (
<div
className={`${className} ${!onSelect ? 'exempt-hover' : ''} ${message.isConstructor ? 'constructor' : ''}`}
key={`${message.identifier}-${index}`}
>
{onSelect && (
message.isConstructor
? (
<Button
className='accessory'
icon='upload'
label={t<string>('deploy')}
onClick={_onSelect}
/>
)
: (
<Button
className='accessory'
icon='play'
label={message.isMutating ? t<string>('exec') : t<string>('read')}
onClick={_onSelect}
/>
)
)}
<div className='info'>
<MessageSignature
asConstructor={message.isConstructor}
message={message}
withTooltip
/>
<div className='docs'>
{message.docs.length
? filterDocs(message.docs).map((line, index) => ((
<div key={`${message.identifier}-docs-${index}`}>{line}</div>
)))
: <i>&nbsp;{t<string>('No documentation provided')}&nbsp;</i>
}
</div>
</div>
{lastResult && lastResult.output && (
<Output
className='result'
isFull
label={t<string>('current value')}
>
{valueToText('Text', lastResult.output)}
</Output>
)}
</div>
);
}
export default React.memo(styled(Message)`
align-items: center;
border-radius: 0.25rem;
display: flex;
padding: 0.25rem 0.75rem 0.25rem 0;
&.disabled {
opacity: 1 !important;
background: #eee !important;
color: #555 !important;
}
.info {
flex: 1 1;
margin-left: 1.5rem;
.docs {
font-size: 0.9rem;
font-weight: normal;
}
}
.result {
min-width: 15rem;
}
&+& {
margin-top: 0.5rem;
}
`);

View File

@ -1,157 +1,90 @@
// Copyright 2017-2020 @polkadot/react-components authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { AbiConstructor, AbiMessage } from '@polkadot/api-contract/types';
import { ContractCallOutcome } from '@polkadot/api-contract/types';
import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { Abi } from '@polkadot/api-contract';
import { classes } from '@polkadot/react-components/util';
import { Button, Expander, IconLink } from '@polkadot/react-components';
import { Abi, PromiseContract } from '@polkadot/api-contract';
import { Expander, IconLink } from '@polkadot/react-components';
import { useApi, useCall } from '@polkadot/react-hooks';
import MessageSignature from './MessageSignature';
import Message from './Message';
import { useTranslation } from '../translate';
export interface Props {
address?: string;
className?: string;
contract?: PromiseContract;
contractAbi: Abi;
isLabelled?: boolean;
isRemovable: boolean;
isRemovable?: boolean;
isWatching?: boolean;
onRemove?: () => void;
onSelect?: (messageIndex: number) => () => void;
onSelect?: (messageIndex: number) => void;
onSelectConstructor?: (constructorIndex: number) => void;
withConstructors?: boolean;
withMessages?: boolean;
}
const NOOP = (): void => undefined;
const READ_ADDR = '0x'.padEnd(66, '0');
function onSelect (props: Props, messageIndex: number): () => void {
return function (): void {
const { address: callAddress, contractAbi: { messages }, onSelect } = props;
if (!callAddress || !messages || !messages[messageIndex]) {
return;
}
onSelect && onSelect(messageIndex)();
};
}
function onSelectConstructor (props: Props, index: number): () => void {
return function (): void {
const { contractAbi: { constructors }, onSelectConstructor } = props;
if (!constructors || !constructors[index]) {
return;
}
onSelectConstructor && onSelectConstructor(index);
};
}
function filterDocs (docs: string[]): string[] {
let skip = false;
return docs
.map((line) => line.trim())
.filter((line) => line)
.filter((line, index): boolean => {
if (skip) {
return false;
} else if (index || line.startsWith('#')) {
skip = true;
return false;
}
return true;
});
}
function renderItem (props: Props, message: AbiConstructor | AbiMessage, index: number, asConstructor: boolean, t: <T = string> (key: string) => T): React.ReactNode {
const { docs = [], identifier } = message;
return (
<div
className={classes('message', !onSelect && 'exempt-hover', asConstructor && 'constructor')}
key={identifier}
>
{!asConstructor && props.onSelect && (
<div className='accessory'>
<Button
className='execute'
icon='play'
label={message.isMutating ? t<string>('exec') : t<string>('read')}
onClick={onSelect(props, index)}
/>
</div>
)}
{asConstructor && props.onSelectConstructor && (
<Button
className='accessory'
icon='upload'
label={t<string>('deploy')}
onClick={onSelectConstructor(props, index)}
/>
)}
<div className='info'>
<MessageSignature
asConstructor={asConstructor}
message={message}
withTooltip
/>
<div className='docs'>
{docs && docs.length > 0
? filterDocs(docs).map((line, index) => ((
<React.Fragment key={`${identifier}-docs-${index}`}>
<span>{line}</span>
<br />
</React.Fragment>
)))
: <i>&nbsp;{t<string>('No documentation provided')}&nbsp;</i>
}
</div>
</div>
</div>
);
}
function renderConstructor (props: Props, index: number, t: <T = string> (key: string) => T): React.ReactNode {
const { contractAbi: { constructors } } = props;
if (!constructors[index]) {
return null;
}
return renderItem(props, constructors[index], index, true, t);
}
function renderMessage (props: Props, index: number, t: <T = string> (key: string) => T): React.ReactNode {
const { contractAbi: { messages } } = props;
if (!messages[index]) {
return null;
}
return renderItem(props, messages[index], index, false, t);
}
function Messages (props: Props): React.ReactElement<Props> {
function Messages ({ className = '', contract, contractAbi: { constructors, messages }, isLabelled, isRemovable, isWatching, onRemove = NOOP, onSelect, onSelectConstructor, withConstructors, withMessages } : Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { className = '', contractAbi: { constructors, messages }, isLabelled, isRemovable, onRemove = NOOP, withConstructors, withMessages } = props;
const { api } = useApi();
const bestNumber = useCall(api.derive.chain.bestNumber);
const [isUpdating, setIsUpdating] = useState(false);
const [lastResults, setLastResults] = useState<(ContractCallOutcome | undefined)[]>([]);
const _onExpander = useCallback(
(isOpen: boolean): void => {
isWatching && setIsUpdating(isOpen);
},
[isWatching]
);
useEffect((): void => {
const maxWeight = api.consts.system.maximumBlockWeight.muln(64).divn(100);
bestNumber && contract && isUpdating && Promise
.all(messages.map((m) =>
m.isMutating || m.args.length !== 0
? Promise.resolve(undefined)
: contract.read(m, 0, maxWeight).send(READ_ADDR)
))
.then(setLastResults)
.catch(console.error);
}, [api, bestNumber, contract, isUpdating, isWatching, messages]);
return (
<div className={`ui--Messages ${className} ${isLabelled ? 'labelled' : ''}`}>
{withConstructors && (
<Expander summary={t<string>('Constructors ({{count}})', { replace: { count: constructors.length } })}>
{constructors.map((_, index) => renderConstructor(props, index, t))}
{constructors.map((message, index) => (
<Message
index={index}
key={index}
message={message}
onSelect={onSelectConstructor}
/>
))}
</Expander>
)}
{withMessages && (
<Expander summary={t<string>('Messages ({{count}})', { replace: { count: constructors.length } })}>
{messages.map((_, index) => renderMessage(props, index, t))}
<Expander
onClick={_onExpander}
summary={t<string>('Messages ({{count}})', { replace: { count: constructors.length } })}
>
{messages.map((message, index) => (
<Message
index={index}
key={index}
lastResult={lastResults[index]}
message={message}
onSelect={onSelect}
/>
))}
</Expander>
)}
{isRemovable && (
@ -167,8 +100,7 @@ function Messages (props: Props): React.ReactElement<Props> {
}
export default React.memo(styled(Messages)`
padding: 0;
margin: 0;
padding-bottom: 0.75rem !important;
.remove-abi {
float: right;
@ -187,38 +119,4 @@ export default React.memo(styled(Messages)`
padding: 1rem 1rem 0.5rem;
width: 100%;
}
.message+.message {
margin-top: 0.5rem;
}
& .message {
align-items: center;
// background: #f8f8f8;
border-radius: 0.25rem;
// color: #4e4e4e;
display: flex;
padding: 0.25rem 0.75rem 0.25rem 0;
// &.constructor {
// background: #e8f4ff;
// }
&.disabled {
opacity: 1 !important;
background: #eee !important;
color: #555 !important;
}
.info {
flex: 1 1;
font-size: 0.9rem;
margin-left: 1.5rem;
.docs {
font-size: 0.8rem;
font-weight: normal;
}
}
}
`);

View File

@ -22,6 +22,7 @@ export interface Props {
helpIcon?: IconName;
isOpen?: boolean;
isPadded?: boolean;
onClick?: (isOpen: boolean) => void;
summary?: React.ReactNode;
summaryHead?: React.ReactNode;
summaryMeta?: Meta;
@ -56,17 +57,20 @@ function formatMeta (meta?: Meta): React.ReactNode | null {
return <>{parts.map((part, index) => index % 2 ? <em key={index}>[{part}]</em> : <span key={index}>{part}</span>)}&nbsp;</>;
}
function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded, summary, summaryHead, summaryMeta, summarySub, withHidden }: Props): React.ReactElement<Props> {
function Expander ({ children, className = '', help, helpIcon, isOpen, isPadded, onClick, summary, summaryHead, summaryMeta, summarySub, withHidden }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isExpanded, toggleExpanded] = useToggle(isOpen);
const [isExpanded, toggleExpanded] = useToggle(isOpen, onClick);
const headerMain = useMemo(
() => summary || formatMeta(summaryMeta),
[summary, summaryMeta]
);
const headerSub = useMemo(
() => summary ? (formatMeta(summaryMeta) || summarySub) : null,
[summary, summaryMeta, summarySub]
);
const hasContent = useMemo(
() => !!children && (!Array.isArray(children) || children.length !== 0),
[children]

View File

@ -1,15 +1,21 @@
// Copyright 2017-2020 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
// Simple wrapper for a true/false toggle
export default function useToggle (defaultValue = false): [boolean, () => void, (value: boolean) => void] {
export default function useToggle (defaultValue = false, onToggle?: (isActive: boolean) => void): [boolean, () => void, (value: boolean) => void] {
const [isActive, setActive] = useState(defaultValue);
const toggleActive = useCallback(
(): void => setActive((isActive: boolean) => !isActive),
() => setActive((isActive: boolean) => !isActive),
[]
);
useEffect(
() => onToggle && onToggle(isActive),
[isActive, onToggle]
);
return [isActive, toggleActive, setActive];
}