diff --git a/packages/apps/public/locales/en/app-contracts.json b/packages/apps/public/locales/en/app-contracts.json index 51f9ed0a0f75..240550f7ae60 100644 --- a/packages/apps/public/locales/en/app-contracts.json +++ b/packages/apps/public/locales/en/app-contracts.json @@ -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", diff --git a/packages/page-contracts/src/Codes/Upload.tsx b/packages/page-contracts/src/Codes/Upload.tsx index 3671866c3b46..0dbded8290d8 100644 --- a/packages/page-contracts/src/Codes/Upload.tsx +++ b/packages/page-contracts/src/Codes/Upload.tsx @@ -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; - - 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; + async function dryRun () { + let contract: SubmittableExtrinsic<'promise'> | null = null; + let error: string | null = null; + + try { + if (code && contractAbi?.constructors[constructorIndex]?.method && value && accountId) { + const dryRunParams: Parameters = + [ + 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 => { diff --git a/packages/page-contracts/src/Contracts/Call.tsx b/packages/page-contracts/src/Contracts/Call.tsx index 4a59607f8ca6..7ec3231ae59d 100644 --- a/packages/page-contracts/src/Contracts/Call.tsx +++ b/packages/page-contracts/src/Contracts/Call.tsx @@ -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 | null { const { t } = useTranslation(); + const { api } = useApi(); const message = contract.abi.messages[messageIndex]; const [accountId, setAccountId] = useAccountId(); const [estimatedWeight, setEstimatedWeight] = useState(null); + const [estimatedWeightV2, setEstimatedWeightV2] = useState(null); const [value, isValueValid, setValue] = useFormField(BN_ZERO); const [outcomes, setOutcomes] = useState([]); const [execTx, setExecTx] = useState | 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 = + [ + 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 )} diff --git a/packages/page-contracts/src/Contracts/Deploy.tsx b/packages/page-contracts/src/Contracts/Deploy.tsx index 8a36fd248c2f..9399a9a1ddef 100644 --- a/packages/page-contracts/src/Contracts/Deploy.tsx +++ b/packages/page-contracts/src/Contracts/Deploy.tsx @@ -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, diff --git a/packages/page-contracts/src/shared/InputMegaGas.tsx b/packages/page-contracts/src/shared/InputMegaGas.tsx index c6700ba3b801..d8eff4d30014 100644 --- a/packages/page-contracts/src/shared/InputMegaGas.tsx +++ b/packages/page-contracts/src/shared/InputMegaGas.tsx @@ -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 { +function InputMegaGas ({ className, + estimatedWeight, + estimatedWeightV2, + isCall, + weight: { executionTime, + isValid, + isWeightV2, + megaGas, + megaRefTime, + percentage, + proofSize, + setIsEmpty, + setMegaGas, + setMegaRefTime, + setProofSize } }: Props): React.ReactElement { 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 ( - - ('max gas allowed (M, {{estimatedMg}} estimated)', { replace: { estimatedMg: estimatedMg.toString() } }) - : t('max gas allowed (M)') - } - labelExtra={(estimatedWeight || isCall) && ( - + {isWeightV2 + ? <> + ('max RefTime allowed (M, {{estimatedRefTime}} estimated)', { replace: { estimatedMgRefTime: estimatedMgRefTime.toString() } }) + : t('max RefTime allowed (M)') + } + onChange={isDisabled ? undefined : setMegaRefTime} + value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : megaRefTime)} + > + {(estimatedWeightV2 || isCall) && ( + ('max read gas') + : t('use estimated gas') + } + onChange={setWithEstimate} + value={withEstimate} + /> + )} + + ('max read gas') - : t('use estimated gas') + estimatedProofSize && (isCall ? !withEstimate : true) + ? t('max ProofSize allowed ({{estimatedProofSize}} estimated)', { replace: { estimatedProofSize: estimatedProofSize.toString() } }) + : t('max ProofSize allowed') } - onChange={setWithEstimate} - value={withEstimate} + onChange={isDisabled ? undefined : setProofSize} + value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : proofSize)} /> - )} - onChange={isDisabled ? undefined : setMegaGas} - value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : megaGas)} - /> -
- {t('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '} - {t('{{percentage}}% of block weight', { replace: { percentage: percentage.toFixed(2) } })} -
-
+
+ {t('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '} + {t('{{percentage}}% of block weight', { replace: { percentage: percentage.toFixed(2) } })} +
+ + : <> + ('max gas allowed (M, {{estimatedMg}} estimated)', { replace: { estimatedMg: estimatedMg.toString() } }) + : t('max gas allowed (M)') + } + onChange={isDisabled ? undefined : setMegaGas} + value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : megaGas)} + > + {(estimatedWeight || isCall) && ( + ('max read gas') + : t('use estimated gas') + } + onChange={setWithEstimate} + value={withEstimate} + /> + )} + +
+ {t('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '} + {t('{{percentage}}% of block weight', { replace: { percentage: percentage.toFixed(2) } })} +
+ } + ); } -const StyledDiv = styled.div` - .contracts--InputMegaGas-meter { - text-align: right; - } -`; - export default React.memo(InputMegaGas); diff --git a/packages/page-contracts/src/types.ts b/packages/page-contracts/src/types.ts index 1b41ba8675a5..5f0f72a4d8c6 100644 --- a/packages/page-contracts/src/types.ts +++ b/packages/page-contracts/src/types.ts @@ -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 setMegaGas: React.Dispatch; + setMegaRefTime: React.Dispatch; + setProofSize: React.Dispatch; weight: BN; + weightV2: WeightV2; } diff --git a/packages/page-contracts/src/useWeight.ts b/packages/page-contracts/src/useWeight.ts index bf0d19b983e8..5245cc3224d1 100644 --- a/packages/page-contracts/src/useWeight.ts +++ b/packages/page-contracts/src/useWeight.ts @@ -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('Weight').proofSize; const [megaGas, _setMegaGas] = useState( 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( + 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( + 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);