Support new contracts putCode (#4622)

* Support new contracts putCode

* New works

* Swap to default constructor as available

* Split into steps
This commit is contained in:
Jaco Greeff 2021-02-06 15:51:09 +01:00 committed by GitHub
parent 7ea7ab774a
commit 1ec6dfd302
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 216 additions and 143 deletions

View File

@ -13,7 +13,7 @@ import styled from 'styled-components';
import { DEV_PHRASE } from '@polkadot/keyring/defaults';
import { getEnvironment } from '@polkadot/react-api/util';
import { AddressRow, Button, Checkbox, CopyButton, Dropdown, Expander, Input, InputAddress, MarkError, MarkWarning, Modal, TextArea } from '@polkadot/react-components';
import { useApi, useLedger } from '@polkadot/react-hooks';
import { useApi, useLedger, useStepper } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { settings } from '@polkadot/ui-settings';
import { isHex, u8aToHex } from '@polkadot/util';
@ -199,7 +199,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
const { isLedgerEnabled } = useLedger();
const [{ address, derivePath, deriveValidation, isSeedValid, pairType, seed, seedType }, setAddress] = useState<AddressState>(() => generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', isEthereum ? 'ethereum' : propsType));
const [isMnemonicSaved, setIsMnemonicSaved] = useState<boolean>(false);
const [step, setStep] = useState(1);
const [step, nextStep, prevStep] = useStepper();
const [isBusy, setIsBusy] = useState(false);
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: false, password: '' });
@ -268,15 +268,6 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
setIsMnemonicSaved(!isMnemonicSaved);
};
const _nextStep = useCallback(
() => setStep((step) => step + 1),
[]
);
const _previousStep = useCallback(
() => setStep((step) => step - 1),
[]
);
const _onCommit = useCallback(
(): void => {
if (!isValid) {
@ -480,7 +471,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
icon='step-forward'
isDisabled={!isFirstStepValid}
label={t<string>('Next')}
onClick={_nextStep}
onClick={nextStep}
/>
}
{step === 2 && (
@ -488,13 +479,13 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={_previousStep}
onClick={prevStep}
/>
<Button
icon='step-forward'
isDisabled={!isSecondStepValid}
label={t<string>('Next')}
onClick={_nextStep}
onClick={nextStep}
/>
</>
)}
@ -503,7 +494,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={_previousStep}
onClick={prevStep}
/>
<Button
icon='plus'

View File

@ -7,15 +7,18 @@ import type { CodeSubmittableResult } from '@polkadot/api-contract/promise/types
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { CodePromise } from '@polkadot/api-contract';
import { InputAddress, InputFile, Modal, TxButton } from '@polkadot/react-components';
import { useAccountId, useApi, useNonEmptyString } from '@polkadot/react-hooks';
import { Button, Dropdown, InputAddress, InputBalance, InputFile, Modal, TxButton } from '@polkadot/react-components';
import { useAccountId, useApi, useNonEmptyString, useNonZeroBn, useStepper } from '@polkadot/react-hooks';
import { Available } from '@polkadot/react-query';
import { keyring } from '@polkadot/ui-keyring';
import { isNull, isWasm } from '@polkadot/util';
import { ABI, InputName } from '../shared';
import { ENDOWMENT } from '../constants';
import { ABI, InputMegaGas, InputName, MessageSignature, Params } from '../shared';
import store from '../store';
import { useTranslation } from '../translate';
import useAbi from '../useAbi';
import useWeight from '../useWeight';
interface Props {
onClose: () => void;
@ -25,10 +28,15 @@ function Upload ({ onClose }: Props): React.ReactElement {
const { t } = useTranslation();
const { api } = useApi();
const [accountId, setAccountId] = useAccountId();
const [step, nextStep, prevStep] = useStepper();
const [uploadTx, setUploadTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
const [constructorIndex, setConstructorIndex] = useState(0);
const [endowment, isEndowmentValid, setEndowment] = useNonZeroBn(ENDOWMENT);
const [params, setParams] = useState<any[]>([]);
const [[wasm, isWasmValid], setWasm] = useState<[Uint8Array | null, boolean]>([null, false]);
const [name, isNameValid, setName] = useNonEmptyString();
const { abiName, contractAbi, errorText, isAbiError, isAbiSupplied, isAbiValid, onChangeAbi, onRemoveAbi } = useAbi();
const weight = useWeight();
const code = useMemo(
() => isAbiValid && isWasmValid && wasm && contractAbi
@ -37,9 +45,32 @@ function Upload ({ onClose }: Props): React.ReactElement {
[api, contractAbi, isAbiValid, isWasmValid, wasm]
);
const constructOptions = useMemo(
() => contractAbi
? contractAbi.constructors.map((message, index) => ({
info: message.identifier,
key: `${index}`,
text: (
<MessageSignature
asConstructor
message={message}
/>
),
value: index
}))
: [],
[contractAbi]
);
useEffect((): void => {
setUploadTx(() => code ? code.createBlueprint() : null);
}, [code]);
setConstructorIndex(() =>
constructOptions.find(({ info }) => info === 'default')?.value || 0
);
}, [constructOptions]);
useEffect((): void => {
setParams([]);
}, [constructorIndex]);
useEffect((): void => {
setWasm(
@ -53,6 +84,20 @@ function Upload ({ onClose }: Props): React.ReactElement {
abiName && setName(abiName);
}, [abiName, setName]);
useEffect((): void => {
let contract: SubmittableExtrinsic<'promise'> | null = null;
try {
contract = code && contractAbi && endowment
? code.createContract(constructorIndex, { gasLimit: weight.weight, value: endowment }, params)
: null;
} catch (error) {
// ignore, is null
}
setUploadTx(() => contract);
}, [code, contractAbi, constructorIndex, endowment, params, weight]);
const _onAddWasm = useCallback(
(wasm: Uint8Array, name: string): void => {
setWasm([wasm, isWasm(wasm)]);
@ -66,75 +111,132 @@ function Upload ({ onClose }: Props): React.ReactElement {
result.blueprint && store
.saveCode(result.blueprint.codeHash, {
abi: JSON.stringify(result.blueprint.abi.json),
name: name || '<unknown>',
name: name || '<>',
tags: []
})
.catch(console.error);
result.contract && keyring.saveContract(result.contract.address.toString(), {
contract: {
abi: JSON.stringify(result.contract.abi.json),
genesisHash: api.genesisHash.toHex()
},
name: name || '<>',
tags: []
});
},
[name]
[api, name]
);
const isSubmittable = !!accountId && (!isNull(name) && isNameValid) && isWasmValid && isAbiSupplied && isAbiValid && !!uploadTx;
const isSubmittable = !!accountId && (!isNull(name) && isNameValid) && isWasmValid && isAbiSupplied && isAbiValid && !!uploadTx && step === 2;
const invalidAbi = isAbiError || !isAbiSupplied;
return (
<Modal header={t('Upload WASM')}>
<Modal header={t('Upload & deploy code {{info}}', { replace: { info: `${step}/2` } })}>
<Modal.Content>
<InputAddress
help={t('Specify the user account to use for this deployment. Any fees will be deducted from this account.')}
isInput={false}
label={t('deployment account')}
labelExtra={
<Available
label={t<string>('transferrable')}
params={accountId}
/>
}
onChange={setAccountId}
type='account'
value={accountId}
/>
<ABI
contractAbi={contractAbi}
errorText={errorText}
isError={invalidAbi}
isSupplied={isAbiSupplied}
isValid={isAbiValid}
label={t<string>('json for either ABI or .contract bundle')}
onChange={onChangeAbi}
onRemove={onRemoveAbi}
withWasm
/>
{!invalidAbi && contractAbi && (
{step === 1 && (
<>
{!contractAbi.project.source.wasm.length && (
<InputFile
help={t<string>('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
isError={!isWasmValid}
label={t<string>('compiled contract WASM')}
onChange={_onAddWasm}
placeholder={
wasm && !isWasmValid
? t<string>('The code is not recognized as being in valid WASM format')
: null
}
/>
<InputAddress
help={t('Specify the user account to use for this deployment. Any fees will be deducted from this account.')}
isInput={false}
label={t('deployment account')}
labelExtra={
<Available
label={t<string>('transferrable')}
params={accountId}
/>
}
onChange={setAccountId}
type='account'
value={accountId}
/>
<ABI
contractAbi={contractAbi}
errorText={errorText}
isError={invalidAbi}
isSupplied={isAbiSupplied}
isValid={isAbiValid}
label={t<string>('json for either ABI or .contract bundle')}
onChange={onChangeAbi}
onRemove={onRemoveAbi}
withWasm
/>
{!invalidAbi && contractAbi && (
<>
{!contractAbi.project.source.wasm.length && (
<InputFile
help={t<string>('The compiled WASM for the contract that you wish to deploy. Each unique code blob will be attached with a code hash that can be used to create new instances.')}
isError={!isWasmValid}
label={t<string>('compiled contract WASM')}
onChange={_onAddWasm}
placeholder={
wasm && !isWasmValid
? t<string>('The code is not recognized as being in valid WASM format')
: null
}
/>
)}
<InputName
isError={!isNameValid}
onChange={setName}
value={name || undefined}
/>
</>
)}
<InputName
isError={!isNameValid}
onChange={setName}
value={name || undefined}
</>
)}
{step === 2 && contractAbi && (
<>
<Dropdown
help={t<string>('The deployment constructor information for this contract, as provided by the ABI.')}
isDisabled={contractAbi.constructors.length <= 1}
label={t('deployment constructor')}
onChange={setConstructorIndex}
options={constructOptions}
value={constructorIndex}
/>
<Params
onChange={setParams}
params={contractAbi.constructors[constructorIndex].args}
registry={contractAbi.registry}
/>
<InputBalance
help={t<string>('The allotted endowment for the deployed contract, i.e. the amount transferred to the contract upon instantiation.')}
isError={!isEndowmentValid}
label={t<string>('endowment')}
onChange={setEndowment}
value={endowment}
/>
<InputMegaGas
help={t<string>('The maximum amount of gas that can be used by this deployment, if the code requires more, the deployment will fail.')}
weight={weight}
/>
</>
)}
</Modal.Content>
<Modal.Actions onCancel={onClose}>
{step === 1
? (
<Button
icon='step-forward'
isDisabled={!code || !contractAbi}
label={t<string>('Next')}
onClick={nextStep}
/>
)
: (
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={prevStep}
/>
)
}
<TxButton
accountId={accountId}
extrinsic={uploadTx}
icon='upload'
isDisabled={!isSubmittable}
label={t('Upload')}
label={t('Deploy')}
onClick={onClose}
onSuccess={_onSuccess}
/>

View File

@ -1,50 +0,0 @@
// Copyright 2017-2021 @polkadot/app-contracts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import styled from 'styled-components';
import { Button } from '@polkadot/react-components';
import { useToggle } from '@polkadot/react-hooks';
import { useTranslation } from '../translate';
interface Props {
className?: string;
}
function Ink3Banner ({ className = '' }: Props): React.ReactElement<Props> | null {
const [isVisible, toggleVisible] = useToggle(true);
const { t } = useTranslation();
if (!isVisible) {
return null;
}
return (
<article className={`${className} warning centered`}>
<div className='box'>
<p>{t<string>('This contracts interface only supports Substrate 2.0+ with contracts using Ink! 3.0+ or Solang 0.1.5+ with the ability to generate the latest metadata formats.')}</p>
<p>{t<string>('Older versions of the contracts ABI cannot be used and will display an error indicating an invalid ABI when uploaded. Ensure you follow the Ink! or solang upgrade instructions if you are using an older version.')}</p>
<p>{t<string>("With the ABI updates, there may still be dragons... the new ABI formats are a complete departure and required a lot of under-the-hood changes. So when something doesn't quite work as expected, please let us know via GH issues.")}</p>
<Button.Group>
<Button
icon='times'
label={t<string>('Understood, close this')}
onClick={toggleVisible}
/>
</Button.Group>
</div>
</article>
);
}
export default React.memo(styled(Ink3Banner)`
.box {
padding: 0 0.5rem;
.ui--Button-Group {
margin-bottom: 0;
}
}
`);

View File

@ -4,7 +4,6 @@
import type { SubmittableExtrinsic } from '@polkadot/api/types';
import type { BlueprintSubmittableResult } from '@polkadot/api-contract/promise/types';
import BN from 'bn.js';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { BlueprintPromise } from '@polkadot/api-contract';
@ -36,7 +35,7 @@ function Deploy ({ codeHash, constructorIndex = 0, onClose, setConstructorIndex
const [initTx, setInitTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
const [params, setParams] = useState<any[]>([]);
const [accountId, isAccountIdValid, setAccountId] = useFormField<string | null>(null);
const [endowment, isEndowmentValid, setEndowment] = useNonZeroBn(new BN(ENDOWMENT));
const [endowment, isEndowmentValid, setEndowment] = useNonZeroBn(ENDOWMENT);
const [salt, setSalt] = useState(() => randomAsHex());
const [withSalt, setWithSalt] = useState(false);
@ -62,6 +61,7 @@ function Deploy ({ codeHash, constructorIndex = 0, onClose, setConstructorIndex
const constructOptions = useMemo(
() => contractAbi
? contractAbi.constructors.map((message, index) => ({
info: message.identifier,
key: `${index}`,
text: (
<MessageSignature

View File

@ -16,7 +16,6 @@ import ContractAdd from '../Contracts/Add';
import ContractsTable from '../Contracts/ContractsTable';
import store from '../store';
import { useTranslation } from '../translate';
import Banner from './Banner';
import Deploy from './Deploy';
import Summary from './Summary';
@ -71,7 +70,7 @@ function Contracts ({ className = '' }: Props): React.ReactElement<Props> {
<Button.Group>
<Button
icon='plus'
label={t('Upload WASM')}
label={t('Upload & deploy code')}
onClick={toggleUpload}
/>
<Button
@ -85,7 +84,6 @@ function Contracts ({ className = '' }: Props): React.ReactElement<Props> {
onClick={toggleAdd}
/>
</Button.Group>
<Banner />
<ContractsTable
contracts={allContracts}
updated={updated}

View File

@ -35,6 +35,7 @@ export { useParaEndpoints } from './useParaEndpoints';
export { usePassword } from './usePassword';
export { useRegistrars } from './useRegistrars';
export { useSavedFlags } from './useSavedFlags';
export { useStepper } from './useStepper';
export { useSudo } from './useSudo';
export { useToggle } from './useToggle';
export { useTreasury } from './useTreasury';

View File

@ -1,7 +1,7 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { isUndefined } from '@polkadot/util';
@ -11,17 +11,20 @@ export type FormField<T> = [
(_?: T | null) => void
];
export function useFormField<T> (defaultValue: T | null, validate: (_: T) => boolean = (): boolean => true): FormField<T> {
type ValidateFn<T> = (_: T) => boolean;
const defaultValidate = (): boolean => true;
export function useFormField<T> (defaultValue: T | null, validate: ValidateFn<T> = defaultValidate): FormField<T> {
const [value, setValue] = useState<T | null>(defaultValue);
const isValid = useMemo(
(): boolean => !!value && validate(value),
() => !!value && validate(value),
[validate, value]
);
const setter = (value?: T | null) => !isUndefined(value) && setValue(value);
const setter = useCallback(
(value?: T | null) => !isUndefined(value) && setValue(value),
[]
);
return [
value,
isValid,
setter
];
return [value, isValid, setter];
}

View File

@ -3,9 +3,10 @@
import { FormField, useFormField } from './useFormField';
export function useNonEmptyString (initialValue = ''): FormField<string> {
return useFormField(
initialValue,
(value?: string | null): boolean => (value && value.length > 0) || false
);
function isValid (value?: string | null): boolean {
return (value && value.length > 0) || false;
}
export function useNonEmptyString (initialValue = ''): FormField<string> {
return useFormField(initialValue, isValid);
}

View File

@ -1,15 +1,20 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import BN from 'bn.js';
import type BN from 'bn.js';
import { BN_ZERO } from '@polkadot/util';
import { useMemo } from 'react';
import { BN_ZERO, bnToBn } from '@polkadot/util';
import { FormField, useFormField } from './useFormField';
export function useNonZeroBn (initialValue: BN = BN_ZERO): FormField<BN> {
return useFormField(
initialValue,
(value: BN): boolean => !value.isZero()
);
function isValid (value: BN): boolean {
return !value.isZero();
}
export function useNonZeroBn (initialValue: BN | number = BN_ZERO): FormField<BN> {
const value = useMemo(() => bnToBn(initialValue), [initialValue]);
return useFormField(value, isValid);
}

View File

@ -0,0 +1,22 @@
// Copyright 2017-2021 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useCallback, useState } from 'react';
type Result = [number, () => void, () => void, (step: number) => void];
export function useStepper (): Result {
const [step, setStep] = useState(1);
const nextStep = useCallback(
() => setStep((step) => step + 1),
[]
);
const prevStep = useCallback(
() => setStep((step) => step - 1),
[]
);
return [step, nextStep, prevStep, setStep];
}