WeightsV2 support for contracts pallet (#8538)

* Weights V2 support

* estimated v2

* instantiate

* [CI Skip] bump/beta 0.124.2-109-x

skip-checks: true

* contract call dry run

* lint fix

* uploading raw wasm type

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Nikhil Ranjan 2023-03-29 14:19:47 +02:00 committed by GitHub
parent 3a6bd19940
commit d317ee8888
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 292 additions and 71 deletions

View File

@ -26,6 +26,7 @@
"Save": "Save",
"The code is not recognized as being in valid WASM format": "The code is not recognized as being in valid WASM format",
"The codeHash is not a valid hex hash": "The codeHash is not a valid hex hash",
"The maximum amount of gas to use for this contract call. If the call requires more, it will fail.": "The maximum amount of gas to use for this contract call. If the call requires more, it will fail.",
"The value is not in a valid address format": "The value is not in a valid address format",
"This operation does not impact the associated on-chain code or any of its contracts.": "This operation does not impact the associated on-chain code or any of its contracts.",
"This operation does not remove the uploaded code WASM and ABI from the chain, nor any deployed contracts. The forget operation only limits your access to the code on this browser.": "This operation does not remove the uploaded code WASM and ABI from the chain, nor any deployed contracts. The forget operation only limits your access to the code on this browser.",
@ -52,6 +53,10 @@
"deployment constructor": "deployment constructor",
"exec": "exec",
"json for either ABI or .contract bundle": "json for either ABI or .contract bundle",
"max ProofSize allowed": "max ProofSize allowed",
"max ProofSize allowed ({{estimatedProofSize}} estimated)": "max ProofSize allowed ({{estimatedProofSize}} estimated)",
"max RefTime allowed (M)": "max RefTime allowed (M)",
"max RefTime allowed (M, {{estimatedRefTime}} estimated)": "max RefTime allowed (M, {{estimatedRefTime}} estimated)",
"max gas allowed (M)": "max gas allowed (M)",
"max gas allowed (M, {{estimatedMg}} estimated)": "max gas allowed (M, {{estimatedMg}} estimated)",
"max read gas": "max read gas",

View File

@ -82,23 +82,42 @@ function Upload ({ onClose }: Props): React.ReactElement {
}, [abiName, setName]);
useEffect((): void => {
let contract: SubmittableExtrinsic<'promise'> | null = null;
let error: string | null = null;
async function dryRun () {
let contract: SubmittableExtrinsic<'promise'> | null = null;
let error: string | null = null;
try {
contract = code && contractAbi?.constructors[constructorIndex]?.method && value
? code.tx[contractAbi.constructors[constructorIndex].method]({
gasLimit: weight.weight,
storageDepositLimit: null,
value: contractAbi?.constructors[constructorIndex].isPayable ? value : undefined
}, ...params)
: null;
} catch (e) {
error = (e as Error).message;
try {
if (code && contractAbi?.constructors[constructorIndex]?.method && value && accountId) {
const dryRunParams: Parameters<typeof api.call.contractsApi.instantiate> =
[
accountId,
contractAbi?.constructors[constructorIndex].isPayable
? api.registry.createType('Balance', value)
: api.registry.createType('Balance', BN_ZERO),
weight.weightV2,
null,
{ Upload: api.registry.createType('Raw', wasm) },
contractAbi?.constructors[constructorIndex]?.toU8a(params),
''
];
const dryRunResult = await api.call.contractsApi.instantiate(...dryRunParams);
contract = code.tx[contractAbi.constructors[constructorIndex].method]({
gasLimit: dryRunResult.gasRequired,
storageDepositLimit: dryRunResult.storageDeposit.isCharge ? dryRunResult.storageDeposit.asCharge : null,
value: contractAbi?.constructors[constructorIndex].isPayable ? value : undefined
}, ...params);
}
} catch (e) {
error = (e as Error).message;
}
setUploadTx(() => [contract, error]);
}
setUploadTx(() => [contract, error]);
}, [code, contractAbi, constructorIndex, value, params, weight]);
dryRun().catch((e) => console.error(e));
}, [accountId, wasm, api, code, contractAbi, constructorIndex, value, params, weight]);
const _onAddWasm = useCallback(
(wasm: Uint8Array, name: string): void => {

View File

@ -4,13 +4,13 @@
import type { SubmittableExtrinsic } from '@polkadot/api/types';
import type { ContractPromise } from '@polkadot/api-contract';
import type { ContractCallOutcome } from '@polkadot/api-contract/types';
import type { WeightV2 } from '@polkadot/types/interfaces';
import type { CallResult } from './types.js';
import React, { useCallback, useEffect, useState } from 'react';
import { Button, Dropdown, Expander, InputAddress, InputBalance, Modal, styled, Toggle, TxButton } from '@polkadot/react-components';
import { useAccountId, useDebounce, useFormField, useToggle } from '@polkadot/react-hooks';
import { convertWeight } from '@polkadot/react-hooks/useWeight';
import { useAccountId, useApi, useDebounce, useFormField, useToggle } from '@polkadot/react-hooks';
import { Available } from '@polkadot/react-query';
import { BN, BN_ONE, BN_ZERO } from '@polkadot/util';
@ -33,9 +33,11 @@ const MAX_CALL_WEIGHT = new BN(5_000_000_000_000).isub(BN_ONE);
function Call ({ className = '', contract, messageIndex, onCallResult, onChangeMessage, onClose }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const message = contract.abi.messages[messageIndex];
const [accountId, setAccountId] = useAccountId();
const [estimatedWeight, setEstimatedWeight] = useState<BN | null>(null);
const [estimatedWeightV2, setEstimatedWeightV2] = useState<WeightV2 | null>(null);
const [value, isValueValid, setValue] = useFormField<BN>(BN_ZERO);
const [outcomes, setOutcomes] = useState<CallResult[]>([]);
const [execTx, setExecTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
@ -47,18 +49,46 @@ function Call ({ className = '', contract, messageIndex, onCallResult, onChangeM
useEffect((): void => {
setEstimatedWeight(null);
setEstimatedWeightV2(null);
setParams([]);
}, [contract, messageIndex]);
useEffect((): void => {
value && message.isMutating && setExecTx((): SubmittableExtrinsic<'promise'> | null => {
try {
return contract.tx[message.method]({ gasLimit: weight.weight, storageDepositLimit: null, value: message.isPayable ? value : 0 }, ...params);
} catch {
return null;
async function dryRun () {
if (accountId && value && message.isMutating) {
const dryRunParams: Parameters<typeof api.call.contractsApi.call> =
[
accountId,
contract.address,
message.isPayable
? api.registry.createType('Balance', value)
: api.registry.createType('Balance', BN_ZERO),
weight.weightV2,
null,
message.toU8a(params)
];
const dryRunResult = await api.call.contractsApi.call(...dryRunParams);
setExecTx((): SubmittableExtrinsic<'promise'> | null => {
try {
return contract.tx[message.method](
{
gasLimit: dryRunResult.gasRequired,
storageDepositLimit: dryRunResult.storageDeposit.isCharge ? dryRunResult.storageDeposit.asCharge : null,
value: message.isPayable ? value : 0
},
...params
);
} catch {
return null;
}
});
}
});
}, [accountId, contract, message, value, weight, params]);
}
dryRun().catch((e) => console.error(e));
}, [api, accountId, contract, message, value, weight, params]);
useEffect((): void => {
if (!accountId || !message || !dbParams || !dbValue) {
@ -67,13 +97,26 @@ function Call ({ className = '', contract, messageIndex, onCallResult, onChangeM
contract
.query[message.method](accountId, { gasLimit: -1, storageDepositLimit: null, value: message.isPayable ? dbValue : 0 }, ...dbParams)
.then(({ gasRequired, result }) => setEstimatedWeight(
result.isOk
? convertWeight(gasRequired).v1Weight
: null
))
.catch(() => setEstimatedWeight(null));
}, [accountId, contract, message, dbParams, dbValue]);
.then(({ gasRequired, result }) => {
if (weight.isWeightV2) {
setEstimatedWeightV2(
result.isOk
? api.registry.createType('WeightV2', gasRequired)
: null
);
} else {
setEstimatedWeight(
result.isOk
? gasRequired.refTime.toBn()
: null
);
}
})
.catch(() => {
setEstimatedWeight(null);
setEstimatedWeightV2(null);
});
}, [api, accountId, contract, message, dbParams, dbValue, weight.isWeightV2]);
const _onSubmitRpc = useCallback(
(): void => {
@ -82,7 +125,11 @@ function Call ({ className = '', contract, messageIndex, onCallResult, onChangeM
}
contract
.query[message.method](accountId, { gasLimit: weight.isEmpty ? -1 : weight.weight, storageDepositLimit: null, value: message.isPayable ? value : 0 }, ...params)
.query[message.method](
accountId,
{ gasLimit: weight.isWeightV2 ? weight.weightV2 : weight.isEmpty ? -1 : weight.weight, storageDepositLimit: null, value: message.isPayable ? value : 0 },
...params
)
.then((result): void => {
setOutcomes([{
...result,
@ -168,6 +215,13 @@ function Call ({ className = '', contract, messageIndex, onCallResult, onChangeM
)}
<InputMegaGas
estimatedWeight={message.isMutating ? estimatedWeight : MAX_CALL_WEIGHT}
estimatedWeightV2={message.isMutating
? estimatedWeightV2
: api.registry.createType('WeightV2', {
proofSize: new BN(1_000_000),
refTIme: MAX_CALL_WEIGHT
})
}
isCall={!message.isMutating}
weight={weight}
/>

View File

@ -79,7 +79,7 @@ function Deploy ({ codeHash, constructorIndex = 0, onClose, setConstructorIndex
if (blueprint && contractAbi?.constructors[constructorIndex]?.method) {
try {
return blueprint.tx[contractAbi.constructors[constructorIndex].method]({
gasLimit: weight.weight,
gasLimit: weight.isWeightV2 ? weight.weightV2 : weight.weight,
salt: withSalt
? salt
: null,

View File

@ -1,12 +1,13 @@
// Copyright 2017-2023 @polkadot/app-contracts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { WeightV2 } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
import type { UseWeight } from '../types.js';
import React, { useEffect, useMemo, useState } from 'react';
import { InputNumber, styled, Toggle } from '@polkadot/react-components';
import { InputNumber, Toggle } from '@polkadot/react-components';
import { BN_MILLION, BN_ONE, BN_ZERO } from '@polkadot/util';
import { useTranslation } from '../translate.js';
@ -14,11 +15,26 @@ import { useTranslation } from '../translate.js';
interface Props {
className?: string;
estimatedWeight?: BN | null;
estimatedWeightV2?: WeightV2 | null;
isCall?: boolean;
weight: UseWeight;
}
function InputMegaGas ({ className, estimatedWeight, isCall, weight: { executionTime, isValid, megaGas, percentage, setIsEmpty, setMegaGas } }: Props): React.ReactElement<Props> {
function InputMegaGas ({ className,
estimatedWeight,
estimatedWeightV2,
isCall,
weight: { executionTime,
isValid,
isWeightV2,
megaGas,
megaRefTime,
percentage,
proofSize,
setIsEmpty,
setMegaGas,
setMegaRefTime,
setProofSize } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [withEstimate, setWithEstimate] = useState(true);
@ -29,10 +45,32 @@ function InputMegaGas ({ className, estimatedWeight, isCall, weight: { execution
[estimatedWeight]
);
const estimatedMgRefTime = useMemo(
() => estimatedWeightV2
? estimatedWeightV2.refTime.toBn().div(BN_MILLION).iadd(BN_ONE)
: null,
[estimatedWeightV2]
);
const estimatedProofSize = useMemo(
() => estimatedWeightV2
? estimatedWeightV2.proofSize.toBn()
: null,
[estimatedWeightV2]
);
useEffect((): void => {
withEstimate && estimatedMg && setMegaGas(estimatedMg);
}, [estimatedMg, setMegaGas, withEstimate]);
useEffect((): void => {
withEstimate && estimatedMgRefTime && setMegaRefTime(estimatedMgRefTime);
}, [estimatedMgRefTime, setMegaRefTime, withEstimate]);
useEffect((): void => {
withEstimate && estimatedProofSize && setProofSize(estimatedProofSize);
}, [estimatedProofSize, setProofSize, withEstimate]);
useEffect((): void => {
setIsEmpty(withEstimate && !!isCall);
}, [isCall, setIsEmpty, withEstimate]);
@ -40,43 +78,85 @@ function InputMegaGas ({ className, estimatedWeight, isCall, weight: { execution
const isDisabled = !!estimatedMg && withEstimate;
return (
<StyledDiv className={className}>
<InputNumber
defaultValue={estimatedMg && isDisabled ? estimatedMg.toString() : undefined}
isDisabled={isDisabled}
isError={!isValid}
isZeroable={isCall}
label={
estimatedMg && (isCall ? !withEstimate : true)
? t<string>('max gas allowed (M, {{estimatedMg}} estimated)', { replace: { estimatedMg: estimatedMg.toString() } })
: t<string>('max gas allowed (M)')
}
labelExtra={(estimatedWeight || isCall) && (
<Toggle
<div className={className}>
{isWeightV2
? <>
<InputNumber
defaultValue={estimatedMgRefTime && isDisabled ? estimatedMgRefTime.toString() : undefined}
isDisabled={isDisabled}
isError={!isValid}
isZeroable={isCall}
label={
isCall
? t<string>('max read gas')
: t<string>('use estimated gas')
estimatedMgRefTime && (isCall ? !withEstimate : true)
? t<string>('max RefTime allowed (M, {{estimatedRefTime}} estimated)', { replace: { estimatedMgRefTime: estimatedMgRefTime.toString() } })
: t<string>('max RefTime allowed (M)')
}
onChange={setWithEstimate}
value={withEstimate}
onChange={isDisabled ? undefined : setMegaRefTime}
value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : megaRefTime)}
>
{(estimatedWeightV2 || isCall) && (
<Toggle
label={
isCall
? t<string>('max read gas')
: t<string>('use estimated gas')
}
onChange={setWithEstimate}
value={withEstimate}
/>
)}
</InputNumber>
<InputNumber
defaultValue={estimatedProofSize && isDisabled ? estimatedProofSize.toString() : undefined}
isDisabled={isDisabled}
isError={!isValid}
isZeroable={isCall}
label={
estimatedProofSize && (isCall ? !withEstimate : true)
? t<string>('max ProofSize allowed ({{estimatedProofSize}} estimated)', { replace: { estimatedProofSize: estimatedProofSize.toString() } })
: t<string>('max ProofSize allowed')
}
onChange={isDisabled ? undefined : setProofSize}
value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : proofSize)}
/>
)}
onChange={isDisabled ? undefined : setMegaGas}
value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : megaGas)}
/>
<div className='contracts--InputMegaGas-meter'>
{t<string>('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '}
{t<string>('{{percentage}}% of block weight', { replace: { percentage: percentage.toFixed(2) } })}
</div>
</StyledDiv>
<div className='contracts--Input-meter'>
{t<string>('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '}
{t<string>('{{percentage}}% of block weight', { replace: { percentage: percentage.toFixed(2) } })}
</div>
</>
: <>
<InputNumber
defaultValue={estimatedMg && isDisabled ? estimatedMg.toString() : undefined}
isDisabled={isDisabled}
isError={!isValid}
isZeroable={isCall}
label={
estimatedMg && (isCall ? !withEstimate : true)
? t<string>('max gas allowed (M, {{estimatedMg}} estimated)', { replace: { estimatedMg: estimatedMg.toString() } })
: t<string>('max gas allowed (M)')
}
onChange={isDisabled ? undefined : setMegaGas}
value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : megaGas)}
>
{(estimatedWeight || isCall) && (
<Toggle
label={
isCall
? t<string>('max read gas')
: t<string>('use estimated gas')
}
onChange={setWithEstimate}
value={withEstimate}
/>
)}
</InputNumber>
<div className='contracts--Input-meter'>
{t<string>('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '}
{t<string>('{{percentage}}% of block weight', { replace: { percentage: percentage.toFixed(2) } })}
</div>
</>}
</div>
);
}
const StyledDiv = styled.div`
.contracts--InputMegaGas-meter {
text-align: right;
}
`;
export default React.memo(InputMegaGas);

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import type { Abi } from '@polkadot/api-contract';
import type { WeightV2 } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
export interface CodeJson {
@ -29,9 +30,15 @@ export interface UseWeight {
executionTime: number;
isEmpty: boolean;
isValid: boolean;
isWeightV2: boolean;
megaGas: BN;
megaRefTime: BN;
proofSize: BN;
percentage: number;
setIsEmpty: React.Dispatch<boolean>
setMegaGas: React.Dispatch<BN | undefined>;
setMegaRefTime: React.Dispatch<BN | undefined>;
setProofSize: React.Dispatch<BN | undefined>;
weight: BN;
weightV2: WeightV2;
}

View File

@ -1,7 +1,7 @@
// Copyright 2017-2023 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Weight } from '@polkadot/types/interfaces';
import type { Weight, WeightV2 } from '@polkadot/types/interfaces';
import type { BN } from '@polkadot/util';
import type { UseWeight } from './types.js';
@ -9,11 +9,12 @@ import { useCallback, useMemo, useState } from 'react';
import { createNamedHook, useApi, useBlockInterval } from '@polkadot/react-hooks';
import { convertWeight } from '@polkadot/react-hooks/useWeight';
import { BN_MILLION, BN_TEN, BN_ZERO } from '@polkadot/util';
import { BN_MILLION, BN_ONE, BN_TEN, BN_ZERO } from '@polkadot/util';
function useWeightImpl (): UseWeight {
const { api } = useApi();
const blockTime = useBlockInterval();
const isWeightV2 = !!api.registry.createType<WeightV2>('Weight').proofSize;
const [megaGas, _setMegaGas] = useState<BN>(
convertWeight(
api.consts.system.blockWeights
@ -21,6 +22,16 @@ function useWeightImpl (): UseWeight {
: api.consts.system.maximumBlockWeight as Weight
).v1Weight.div(BN_MILLION).div(BN_TEN)
);
const [megaRefTime, _setMegaRefTime] = useState<BN>(
api.consts.system.blockWeights
? api.consts.system.blockWeights.perClass.normal.maxExtrinsic.unwrapOrDefault().refTime.toBn().div(BN_MILLION).div(BN_TEN)
: BN_ZERO
);
const [proofSize, _setProofSize] = useState<BN>(
api.consts.system.blockWeights
? api.consts.system.blockWeights.perClass.normal.maxExtrinsic.unwrapOrDefault().proofSize.toBn()
: BN_ZERO
);
const [isEmpty, setIsEmpty] = useState(false);
const setMegaGas = useCallback(
@ -32,11 +43,33 @@ function useWeightImpl (): UseWeight {
).v1Weight.div(BN_MILLION).div(BN_TEN)),
[api]
);
const setMegaRefTime = useCallback(
(value?: BN | undefined) =>
_setMegaRefTime(
value || api.consts.system.blockWeights
? api.consts.system.blockWeights.perClass.normal.maxExtrinsic.unwrapOrDefault().refTime.toBn().div(BN_MILLION).div(BN_TEN)
: BN_ZERO
),
[api]
);
const setProofSize = useCallback(
(value?: BN | undefined) =>
_setProofSize(
value || api.consts.system.blockWeights
? api.consts.system.blockWeights.perClass.normal.maxExtrinsic.unwrapOrDefault().proofSize.toBn()
: BN_ZERO
),
[api]
);
return useMemo((): UseWeight => {
let executionTime = 0;
let percentage = 0;
let weight = BN_ZERO;
let weightV2 = api.registry.createType('WeightV2', {
proofSize: BN_ZERO,
refTime: BN_ZERO
});
let isValid = false;
if (megaGas) {
@ -53,17 +86,40 @@ function useWeightImpl (): UseWeight {
isValid = !megaGas.isZero() && percentage < 65;
}
if (isWeightV2 && megaRefTime && proofSize) {
weightV2 = api.registry.createType('WeightV2', {
proofSize,
refTime: megaRefTime.mul(BN_MILLION)
});
executionTime = megaRefTime.mul(BN_MILLION).mul(blockTime).div(
api.consts.system.blockWeights
? api.consts.system.blockWeights.perClass.normal.maxExtrinsic.unwrapOrDefault().refTime.toBn()
: BN_ONE
).toNumber();
percentage = (executionTime / blockTime.toNumber()) * 100;
// execution is 2s of 6s blocks, i.e. 1/3
executionTime = executionTime / 3000;
isValid = !megaRefTime.isZero(); // && percentage < 65;
}
return {
executionTime,
isEmpty,
isValid: isEmpty || isValid,
isWeightV2,
megaGas: megaGas || BN_ZERO,
megaRefTime: megaRefTime || BN_ZERO,
percentage,
proofSize: proofSize || BN_ZERO,
setIsEmpty,
setMegaGas,
weight
setMegaRefTime,
setProofSize,
weight,
weightV2
};
}, [api, blockTime, isEmpty, megaGas, setIsEmpty, setMegaGas]);
}, [api, blockTime, isEmpty, isWeightV2, megaGas, megaRefTime, proofSize, setIsEmpty, setMegaGas, setMegaRefTime, setProofSize]);
}
export default createNamedHook('useWeight', useWeightImpl);