Watch contract state (#3838)
* Watch contract state * Display actual outputs
This commit is contained in:
parent
e28181cdb1
commit
6bdb12a2ec
@ -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 (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -34,7 +34,6 @@ function renderMessages ({ contractAbi, onRemove, onSelectConstructor, withConst
|
||||
<Messages
|
||||
contractAbi={contractAbi}
|
||||
isLabelled={withLabel}
|
||||
isRemovable={false}
|
||||
onRemove={onRemove}
|
||||
onSelectConstructor={onSelectConstructor}
|
||||
withConstructors={withConstructors}
|
||||
|
||||
132
packages/page-contracts/src/shared/Message.tsx
Normal file
132
packages/page-contracts/src/shared/Message.tsx
Normal 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> {t<string>('No documentation provided')} </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;
|
||||
}
|
||||
`);
|
||||
@ -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> {t<string>('No documentation provided')} </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;
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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>)} </>;
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
@ -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];
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user