mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-12 10:01:18 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, Input, Modal } from '@pezkuwi/react-components';
|
||||
import { isNull } from '@pezkuwi/util';
|
||||
|
||||
import { ABI, InputName } from '../shared/index.js';
|
||||
import store from '../store.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useAbi from '../useAbi.js';
|
||||
import ValidateCode from './ValidateCode.js';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Add ({ onClose }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [codeHash, setCodeHash] = useState('');
|
||||
const [isCodeHashValid, setIsCodeHashValid] = useState(false);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const { abi, contractAbi, errorText, isAbiError, isAbiSupplied, isAbiValid, onChangeAbi, onRemoveAbi } = useAbi();
|
||||
|
||||
const _onSave = useCallback(
|
||||
(): void => {
|
||||
if (!codeHash || !name) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.saveCode(codeHash, { abi, name, tags: [] });
|
||||
onClose();
|
||||
},
|
||||
[abi, codeHash, name, onClose]
|
||||
);
|
||||
|
||||
const isNameValid = !isNull(name) && name.length > 0;
|
||||
const isValid = isCodeHashValid && isNameValid && isAbiSupplied && isAbiValid;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header={t('Add an existing code hash')}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Content>
|
||||
<Input
|
||||
autoFocus
|
||||
isError={codeHash.length > 0 && !isCodeHashValid}
|
||||
label={t('code hash')}
|
||||
onChange={setCodeHash}
|
||||
value={codeHash}
|
||||
/>
|
||||
<ValidateCode
|
||||
codeHash={codeHash}
|
||||
onChange={setIsCodeHashValid}
|
||||
/>
|
||||
<InputName
|
||||
isError={!isNameValid}
|
||||
onChange={setName}
|
||||
value={name || undefined}
|
||||
/>
|
||||
<ABI
|
||||
contractAbi={contractAbi}
|
||||
errorText={errorText}
|
||||
isError={isAbiError || !isAbiError}
|
||||
isSupplied={isAbiSupplied}
|
||||
isValid={isAbiValid}
|
||||
onChange={onChangeAbi}
|
||||
onRemove={onRemoveAbi}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
icon='save'
|
||||
isDisabled={!isValid}
|
||||
label={t('Save')}
|
||||
onClick={_onSave}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Add);
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { Codec } from '@pezkuwi/types/types';
|
||||
import type { CodeStored } from '../types.js';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, Card, CopyButton, Forget, styled } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { CodeRow, Messages } from '../shared/index.js';
|
||||
import store from '../store.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useAbi from '../useAbi.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
code: CodeStored;
|
||||
onShowDeploy: (codeHash: string, constructorIndex: number) => void;
|
||||
}
|
||||
|
||||
function Code ({ className, code, onShowDeploy }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const optCode = useCall<Option<Codec>>(api.query.contracts.pristineCode || api.query.contracts.codeStorage, [code.json.codeHash]);
|
||||
const [isForgetOpen, toggleIsForgetOpen] = useToggle();
|
||||
const { contractAbi } = useAbi([code.json.abi, code.contractAbi], code.json.codeHash, true);
|
||||
|
||||
const _onShowDeploy = useCallback(
|
||||
() => onShowDeploy(code.json.codeHash, 0),
|
||||
[code, onShowDeploy]
|
||||
);
|
||||
|
||||
const _onDeployConstructor = useCallback(
|
||||
(constructorIndex?: number): void => {
|
||||
onShowDeploy && onShowDeploy(code.json.codeHash, constructorIndex || 0);
|
||||
},
|
||||
[code, onShowDeploy]
|
||||
);
|
||||
|
||||
const _onForget = useCallback(
|
||||
(): void => {
|
||||
try {
|
||||
store.forgetCode(code.json.codeHash);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
toggleIsForgetOpen();
|
||||
}
|
||||
},
|
||||
[code, toggleIsForgetOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTr className={className}>
|
||||
<td className='address top'>
|
||||
<Card>
|
||||
<CodeRow
|
||||
code={code}
|
||||
withTags={false}
|
||||
/>
|
||||
{isForgetOpen && (
|
||||
<Forget
|
||||
key='modal-forget-account'
|
||||
mode='code'
|
||||
onClose={toggleIsForgetOpen}
|
||||
onForget={_onForget}
|
||||
>
|
||||
<CodeRow
|
||||
code={code || ''}
|
||||
isInline
|
||||
>
|
||||
<p>{t('You are about to remove this code from your list of available code hashes. Once completed, should you need to access it again, you will have to manually add the code hash again.')}</p>
|
||||
<p>{t('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.')}</p>
|
||||
</CodeRow>
|
||||
</Forget>
|
||||
)}
|
||||
</Card>
|
||||
</td>
|
||||
<td className='all top'>
|
||||
{contractAbi && (
|
||||
<Messages
|
||||
contractAbi={contractAbi}
|
||||
onSelectConstructor={_onDeployConstructor}
|
||||
withConstructors
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='together codeHash'>
|
||||
<div>{`${code.json.codeHash.slice(0, 8)}…${code.json.codeHash.slice(-6)}`}</div>
|
||||
<CopyButton value={code.json.codeHash} />
|
||||
</td>
|
||||
<td className='start together'>
|
||||
{optCode && (
|
||||
optCode.isSome ? t('Available') : t('Not on-chain')
|
||||
)}
|
||||
</td>
|
||||
<td className='button'>
|
||||
<Button
|
||||
icon='trash'
|
||||
onClick={toggleIsForgetOpen}
|
||||
/>
|
||||
{!contractAbi && (
|
||||
<Button
|
||||
icon='upload'
|
||||
label={t('deploy')}
|
||||
onClick={_onShowDeploy}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</StyledTr>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTr = styled.tr`
|
||||
.codeHash {
|
||||
div {
|
||||
display: inline;
|
||||
|
||||
&:first-child {
|
||||
font-family: monospace;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Code);
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
|
||||
import type { CodeSubmittableResult } from '@pezkuwi/api-contract/promise/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { CodePromise } from '@pezkuwi/api-contract';
|
||||
import { Button, Dropdown, InputAddress, InputBalance, InputFile, MarkError, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useAccountId, useApi, useFormField, useNonEmptyString, useStepper } from '@pezkuwi/react-hooks';
|
||||
import { Available } from '@pezkuwi/react-query';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
import { BN_ZERO, isNull, isWasm, stringify } from '@pezkuwi/util';
|
||||
|
||||
import { ABI, InputMegaGas, InputName, MessageSignature, Params } from '../shared/index.js';
|
||||
import store from '../store.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useAbi from '../useAbi.js';
|
||||
import useWeight from '../useWeight.js';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Upload ({ onClose }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [accountId, setAccountId] = useAccountId();
|
||||
const [step, nextStep, prevStep] = useStepper();
|
||||
const [[uploadTx, error], setUploadTx] = useState<[SubmittableExtrinsic<'promise'> | null, string | null]>([null, null]);
|
||||
const [constructorIndex, setConstructorIndex] = useState<number>(0);
|
||||
const [value, isValueValid, setValue] = useFormField<BN>(BN_ZERO);
|
||||
const [params, setParams] = useState<unknown[]>([]);
|
||||
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
|
||||
? new CodePromise(api, contractAbi, wasm)
|
||||
: null,
|
||||
[api, contractAbi, isAbiValid, isWasmValid, wasm]
|
||||
);
|
||||
|
||||
const constructOptions = useMemo(
|
||||
() => contractAbi
|
||||
? contractAbi.constructors.map((c, index) => ({
|
||||
info: c.identifier,
|
||||
key: c.identifier,
|
||||
text: (
|
||||
<MessageSignature
|
||||
asConstructor
|
||||
message={c}
|
||||
/>
|
||||
),
|
||||
value: index
|
||||
}))
|
||||
: [],
|
||||
[contractAbi]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
setConstructorIndex(0);
|
||||
}, [constructOptions]);
|
||||
|
||||
useEffect((): void => {
|
||||
setParams([]);
|
||||
}, [contractAbi, constructorIndex]);
|
||||
|
||||
useEffect((): void => {
|
||||
setWasm(
|
||||
contractAbi && isWasm(contractAbi.info.source.wasm)
|
||||
? [contractAbi.info.source.wasm, true]
|
||||
: [null, false]
|
||||
);
|
||||
}, [contractAbi]);
|
||||
|
||||
useEffect((): void => {
|
||||
abiName && setName(abiName);
|
||||
}, [abiName, setName]);
|
||||
|
||||
useEffect((): void => {
|
||||
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<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]);
|
||||
}
|
||||
|
||||
dryRun().catch((e) => console.error(e));
|
||||
}, [accountId, wasm, api, code, contractAbi, constructorIndex, value, params, weight]);
|
||||
|
||||
const _onAddWasm = useCallback(
|
||||
(wasm: Uint8Array, name: string): void => {
|
||||
setWasm([wasm, isWasm(wasm)]);
|
||||
setName(name.replace('.wasm', '').replace('_', ' '));
|
||||
},
|
||||
[setName]
|
||||
);
|
||||
|
||||
const _onSuccess = useCallback(
|
||||
(result: CodeSubmittableResult): void => {
|
||||
result.blueprint && store.saveCode(result.blueprint.codeHash, {
|
||||
abi: stringify(result.blueprint.abi.json),
|
||||
name: name || '<>',
|
||||
tags: []
|
||||
});
|
||||
result.contract && keyring.saveContract(result.contract.address.toString(), {
|
||||
contract: {
|
||||
abi: stringify(result.contract.abi.json),
|
||||
genesisHash: api.genesisHash.toHex()
|
||||
},
|
||||
name: name || '<>',
|
||||
tags: []
|
||||
});
|
||||
},
|
||||
[api, name]
|
||||
);
|
||||
|
||||
const isSubmittable = !!accountId && (!isNull(name) && isNameValid) && isWasmValid && isAbiSupplied && isAbiValid && !!uploadTx && step === 2;
|
||||
const invalidAbi = isAbiError || !isAbiSupplied;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header={t('Upload & deploy code {{info}}', { replace: { info: `${step}/2` } })}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Content>
|
||||
{step === 1 && (
|
||||
<>
|
||||
<InputAddress
|
||||
isInput={false}
|
||||
label={t('deployment account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={t('transferable')}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
<ABI
|
||||
contractAbi={contractAbi}
|
||||
errorText={errorText}
|
||||
isError={invalidAbi}
|
||||
isSupplied={isAbiSupplied}
|
||||
isValid={isAbiValid}
|
||||
label={t('json for either ABI or .contract bundle')}
|
||||
onChange={onChangeAbi}
|
||||
onRemove={onRemoveAbi}
|
||||
withWasm
|
||||
/>
|
||||
{!invalidAbi && contractAbi && (
|
||||
<>
|
||||
{!contractAbi.info.source.wasm.length && (
|
||||
<InputFile
|
||||
isError={!isWasmValid}
|
||||
label={t('compiled contract WASM')}
|
||||
onChange={_onAddWasm}
|
||||
placeholder={wasm && !isWasmValid && t('The code is not recognized as being in valid WASM format')}
|
||||
/>
|
||||
)}
|
||||
<InputName
|
||||
isError={!isNameValid}
|
||||
onChange={setName}
|
||||
value={name || undefined}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{step === 2 && contractAbi && (
|
||||
<>
|
||||
<Dropdown
|
||||
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}
|
||||
/>
|
||||
{contractAbi.constructors[constructorIndex].isPayable && (
|
||||
<InputBalance
|
||||
isError={!isValueValid}
|
||||
isZeroable
|
||||
label={t('value')}
|
||||
onChange={setValue}
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<InputMegaGas
|
||||
weight={weight}
|
||||
/>
|
||||
{error && (
|
||||
<MarkError content={error} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
{step === 1
|
||||
? (
|
||||
<Button
|
||||
icon='step-forward'
|
||||
isDisabled={!code || !contractAbi}
|
||||
label={t('Next')}
|
||||
onClick={nextStep}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
icon='step-backward'
|
||||
label={t('Prev')}
|
||||
onClick={prevStep}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={uploadTx}
|
||||
icon='upload'
|
||||
isDisabled={!isSubmittable}
|
||||
label={t('Deploy')}
|
||||
onClick={onClose}
|
||||
onSuccess={_onSuccess}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Upload);
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { PrefabWasmModule } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { InfoForInput } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { isHex } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
codeHash?: string | null;
|
||||
onChange: React.Dispatch<boolean>;
|
||||
}
|
||||
|
||||
function ValidateCode ({ codeHash, onChange }: Props): React.ReactElement<Props> | null {
|
||||
const { api } = useApi();
|
||||
const { t } = useTranslation();
|
||||
const optCode = useCall<Option<PrefabWasmModule>>((api.query.contracts || api.query.contract).pristineCode || (api.query.contracts || api.query.contract).codeStorage, [codeHash]);
|
||||
const [isValidHex, isValid] = useMemo(
|
||||
(): [boolean, boolean] => {
|
||||
const isValidHex = !!codeHash && isHex(codeHash) && codeHash.length === 66;
|
||||
const isStored = !!optCode && optCode.isSome;
|
||||
const isValid = isValidHex && isStored;
|
||||
|
||||
onChange(isValid);
|
||||
|
||||
return [
|
||||
isValidHex,
|
||||
isValid
|
||||
];
|
||||
},
|
||||
[codeHash, optCode, onChange]
|
||||
);
|
||||
|
||||
if (isValid || !isValidHex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoForInput type='error'>
|
||||
{
|
||||
isValidHex
|
||||
? t('Unable to find on-chain WASM code for the supplied codeHash')
|
||||
: t('The codeHash is not a valid hex hash')
|
||||
}
|
||||
</InfoForInput>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ValidateCode);
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import contracts from '../store.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Code from './Code.js';
|
||||
|
||||
interface Props {
|
||||
onShowDeploy: (codeHash: string, constructorIndex: number) => void;
|
||||
updated: number;
|
||||
}
|
||||
|
||||
function Codes ({ onShowDeploy }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<[string?, string?, number?][]>([
|
||||
[t('code hashes'), 'start'],
|
||||
[],
|
||||
[],
|
||||
[t('status'), 'start'],
|
||||
[]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
empty={t('No code hashes available')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{contracts.getAllCode().map((code): React.ReactNode => (
|
||||
<Code
|
||||
code={code}
|
||||
key={code.json.codeHash}
|
||||
onShowDeploy={onShowDeploy}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Codes);
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ActionStatus } from '@pezkuwi/react-components/Status/types';
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { AddressRow, Button, Input, Modal } from '@pezkuwi/react-components';
|
||||
import { useApi, useNonEmptyString } from '@pezkuwi/react-hooks';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
|
||||
import { ABI, InputName } from '../shared/index.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useAbi from '../useAbi.js';
|
||||
import ValidateAddr from './ValidateAddr.js';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Add ({ onClose }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [address, setAddress] = useState<string | null>(null);
|
||||
const [isAddressValid, setIsAddressValid] = useState(false);
|
||||
const [name, isNameValid, setName] = useNonEmptyString('New Contract');
|
||||
const { abi, contractAbi, errorText, isAbiError, isAbiSupplied, isAbiValid, onChangeAbi, onRemoveAbi } = useAbi([null, null], null, true);
|
||||
|
||||
const _onAdd = useCallback(
|
||||
(): void => {
|
||||
const status: Partial<ActionStatus> = { action: 'create' };
|
||||
|
||||
if (!address || !abi || !name) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const json = {
|
||||
contract: {
|
||||
abi,
|
||||
genesisHash: api.genesisHash.toHex()
|
||||
},
|
||||
name,
|
||||
tags: []
|
||||
};
|
||||
|
||||
keyring.saveContract(address, json);
|
||||
|
||||
status.account = address;
|
||||
status.status = address ? 'success' : 'error';
|
||||
status.message = 'contract added';
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
status.status = 'error';
|
||||
status.message = (error as Error).message;
|
||||
}
|
||||
},
|
||||
[abi, address, api, name, onClose]
|
||||
);
|
||||
|
||||
const isValid = isAddressValid && isNameValid && isAbiValid;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header={t('Add an existing contract')}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Content>
|
||||
<AddressRow
|
||||
defaultName={name}
|
||||
isValid
|
||||
value={address || null}
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
isError={!isAddressValid}
|
||||
label={t('contract address')}
|
||||
onChange={setAddress}
|
||||
value={address || ''}
|
||||
/>
|
||||
<ValidateAddr
|
||||
address={address}
|
||||
onChange={setIsAddressValid}
|
||||
/>
|
||||
<InputName
|
||||
isContract
|
||||
isError={!isNameValid}
|
||||
onChange={setName}
|
||||
value={name || undefined}
|
||||
/>
|
||||
<ABI
|
||||
contractAbi={contractAbi}
|
||||
errorText={errorText}
|
||||
isError={isAbiError || !isAbiValid}
|
||||
isSupplied={isAbiSupplied}
|
||||
isValid={isAbiValid}
|
||||
onChange={onChangeAbi}
|
||||
onRemove={onRemoveAbi}
|
||||
/>
|
||||
</AddressRow>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
icon='save'
|
||||
isDisabled={!isValid}
|
||||
label={t('Save')}
|
||||
onClick={_onAdd}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Add);
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
|
||||
import type { ContractPromise } from '@pezkuwi/api-contract';
|
||||
import type { ContractCallOutcome } from '@pezkuwi/api-contract/types';
|
||||
import type { WeightV2 } from '@pezkuwi/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 '@pezkuwi/react-components';
|
||||
import { useAccountId, useApi, useDebounce, useFormField, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { Available } from '@pezkuwi/react-query';
|
||||
import { BN, BN_ONE, BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import { InputMegaGas, Params } from '../shared/index.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useWeight from '../useWeight.js';
|
||||
import Outcome from './Outcome.js';
|
||||
import { getCallMessageOptions } from './util.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
contract: ContractPromise;
|
||||
messageIndex: number;
|
||||
onCallResult?: (messageIndex: number, result?: ContractCallOutcome) => void;
|
||||
onChangeMessage: (messageIndex: number) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
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);
|
||||
const [params, setParams] = useState<unknown[]>([]);
|
||||
const [isViaCall, toggleViaCall] = useToggle();
|
||||
const weight = useWeight();
|
||||
const dbValue = useDebounce(value);
|
||||
const dbParams = useDebounce(params);
|
||||
|
||||
useEffect((): void => {
|
||||
setEstimatedWeight(null);
|
||||
setEstimatedWeightV2(null);
|
||||
setParams([]);
|
||||
}, [contract, messageIndex]);
|
||||
|
||||
useEffect((): void => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dryRun().catch((e) => console.error(e));
|
||||
}, [api, accountId, contract, message, value, weight, params]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (!accountId || !message || !dbParams || !dbValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
contract
|
||||
.query[message.method](accountId, { gasLimit: -1, storageDepositLimit: null, value: message.isPayable ? dbValue : 0 }, ...dbParams)
|
||||
.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 => {
|
||||
if (!accountId || !message || !value || !weight) {
|
||||
return;
|
||||
}
|
||||
|
||||
contract
|
||||
.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,
|
||||
from: accountId,
|
||||
message,
|
||||
params,
|
||||
when: new Date()
|
||||
}, ...outcomes]);
|
||||
onCallResult && onCallResult(messageIndex, result);
|
||||
})
|
||||
.catch((error): void => {
|
||||
console.error(error);
|
||||
onCallResult && onCallResult(messageIndex);
|
||||
});
|
||||
},
|
||||
[accountId, contract.query, message, messageIndex, onCallResult, outcomes, params, value, weight]
|
||||
);
|
||||
|
||||
const _onClearOutcome = useCallback(
|
||||
(outcomeIndex: number) =>
|
||||
() => setOutcomes([...outcomes.filter((_, index) => index !== outcomeIndex)]),
|
||||
[outcomes]
|
||||
);
|
||||
|
||||
const isValid = !!(accountId && weight.isValid && isValueValid);
|
||||
const isViaRpc = (isViaCall || (!message.isMutating && !message.isPayable));
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
className={`${className} app--contracts-Modal`}
|
||||
header={t('Call a contract')}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
label={t('contract to use')}
|
||||
type='contract'
|
||||
value={contract.address}
|
||||
/>
|
||||
<InputAddress
|
||||
defaultValue={accountId}
|
||||
label={t('call from account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={t('transferable')}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
{messageIndex !== null && (
|
||||
<>
|
||||
<Dropdown
|
||||
defaultValue={messageIndex}
|
||||
isError={message === null}
|
||||
label={t('message to send')}
|
||||
onChange={onChangeMessage}
|
||||
options={getCallMessageOptions(contract)}
|
||||
value={messageIndex}
|
||||
/>
|
||||
<Params
|
||||
onChange={setParams}
|
||||
params={
|
||||
message
|
||||
? message.args
|
||||
: undefined
|
||||
}
|
||||
registry={contract.abi.registry}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{message.isPayable && (
|
||||
<InputBalance
|
||||
isError={!isValueValid}
|
||||
isZeroable
|
||||
label={t('value')}
|
||||
onChange={setValue}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
{message.isMutating && (
|
||||
<Toggle
|
||||
className='rpc-toggle'
|
||||
label={t('read contract only, no execution')}
|
||||
onChange={toggleViaCall}
|
||||
value={isViaCall}
|
||||
/>
|
||||
)}
|
||||
{outcomes.length > 0 && (
|
||||
<Expander
|
||||
className='outcomes'
|
||||
isOpen
|
||||
summary={t('Call results')}
|
||||
>
|
||||
{outcomes.map((outcome, index): React.ReactNode => (
|
||||
<Outcome
|
||||
key={`outcome-${index}`}
|
||||
onClear={_onClearOutcome(index)}
|
||||
outcome={outcome}
|
||||
/>
|
||||
))}
|
||||
</Expander>
|
||||
)}
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
{isViaRpc
|
||||
? (
|
||||
<Button
|
||||
icon='sign-in-alt'
|
||||
isDisabled={!isValid}
|
||||
label={t('Read')}
|
||||
onClick={_onSubmitRpc}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={execTx}
|
||||
icon='sign-in-alt'
|
||||
isDisabled={!isValid || !execTx}
|
||||
label={t('Execute')}
|
||||
onStart={onClose}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Modal.Actions>
|
||||
</StyledModal>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledModal = styled(Modal)`
|
||||
.rpc-toggle {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.clear-all {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.outcomes {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Call);
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ContractPromise } from '@pezkuwi/api-contract';
|
||||
import type { ContractCallOutcome } from '@pezkuwi/api-contract/types';
|
||||
import type { ActionStatus } from '@pezkuwi/react-components/Status/types';
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { ContractInfo } from '@pezkuwi/types/interfaces';
|
||||
import type { ContractLink } from './types.js';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AddressInfo, AddressMini, Button, Forget, styled } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall, useToggle } from '@pezkuwi/react-hooks';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
import { isUndefined } from '@pezkuwi/util';
|
||||
|
||||
import Messages from '../shared/Messages.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
contract: ContractPromise;
|
||||
index: number;
|
||||
links?: ContractLink[];
|
||||
onCall: (contractIndex: number, messaeIndex: number, resultCb: (messageIndex: number, result?: ContractCallOutcome) => void) => void;
|
||||
}
|
||||
|
||||
function transformInfo (optInfo: Option<ContractInfo>): ContractInfo | null {
|
||||
return optInfo.unwrapOr(null);
|
||||
}
|
||||
|
||||
function Contract ({ className, contract, index, links, onCall }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const info = useCall<ContractInfo | null>(api.query.contracts.contractInfoOf, [contract.address], { transform: transformInfo });
|
||||
const [isForgetOpen, toggleIsForgetOpen] = useToggle();
|
||||
|
||||
const _onCall = useCallback(
|
||||
(messageIndex: number, resultCb: (messageIndex: number, result?: ContractCallOutcome) => void) =>
|
||||
onCall(index, messageIndex, resultCb),
|
||||
[index, onCall]
|
||||
);
|
||||
|
||||
const _onForget = useCallback(
|
||||
(): void => {
|
||||
const status: Partial<ActionStatus> = {
|
||||
account: contract.address,
|
||||
action: 'forget'
|
||||
};
|
||||
|
||||
try {
|
||||
keyring.forgetContract(contract.address.toString());
|
||||
status.status = 'success';
|
||||
status.message = t('address forgotten');
|
||||
} catch (error) {
|
||||
status.status = 'error';
|
||||
status.message = (error as Error).message;
|
||||
}
|
||||
|
||||
toggleIsForgetOpen();
|
||||
},
|
||||
[contract.address, t, toggleIsForgetOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTr className={className}>
|
||||
<td className='address top'>
|
||||
{isForgetOpen && (
|
||||
<Forget
|
||||
address={contract.address.toString()}
|
||||
key='modal-forget-contract'
|
||||
mode='contract'
|
||||
onClose={toggleIsForgetOpen}
|
||||
onForget={_onForget}
|
||||
/>
|
||||
)}
|
||||
<AddressMini value={contract.address} />
|
||||
</td>
|
||||
<td className='all top'>
|
||||
<Messages
|
||||
contract={contract}
|
||||
contractAbi={contract.abi}
|
||||
isWatching
|
||||
onSelect={_onCall}
|
||||
trigger={links?.length}
|
||||
withMessages
|
||||
/>
|
||||
</td>
|
||||
<td className='top'>
|
||||
{links?.map(({ blockHash, blockNumber }, index): React.ReactNode => (
|
||||
<a
|
||||
href={`#/explorer/query/${blockHash}`}
|
||||
key={`${index}-${blockNumber}`}
|
||||
>#{blockNumber}</a>
|
||||
))}
|
||||
</td>
|
||||
<td className='number'>
|
||||
<AddressInfo
|
||||
address={contract.address}
|
||||
withBalance
|
||||
withBalanceToggle
|
||||
/>
|
||||
</td>
|
||||
<td className='start together'>
|
||||
{!isUndefined(info) && (
|
||||
info
|
||||
? info.type
|
||||
: t('Not on-chain')
|
||||
)}
|
||||
</td>
|
||||
<td className='button'>
|
||||
<Button
|
||||
icon='trash'
|
||||
onClick={toggleIsForgetOpen}
|
||||
/>
|
||||
</td>
|
||||
</StyledTr>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTr = styled.tr`
|
||||
td.top a+a {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Contract);
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { ContractPromise } from '@pezkuwi/api-contract';
|
||||
import type { ContractCallOutcome } from '@pezkuwi/api-contract/types';
|
||||
import type { SignedBlockExtended } from '@pezkuwi/api-derive/types';
|
||||
import type { ContractLink } from './types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Call from './Call.js';
|
||||
import Contract from './Contract.js';
|
||||
import { getContractForAddress } from './util.js';
|
||||
|
||||
export interface Props {
|
||||
contracts: string[];
|
||||
updated: number;
|
||||
}
|
||||
|
||||
interface Indexes {
|
||||
contractIndex: number;
|
||||
messageIndex: number;
|
||||
onCallResult?: (messageIndex: number, result?: ContractCallOutcome) => void;
|
||||
}
|
||||
|
||||
function filterContracts (api: ApiPromise, keyringContracts: string[] = []): ContractPromise[] {
|
||||
return keyringContracts
|
||||
.map((address) => getContractForAddress(api, address.toString()))
|
||||
.filter((contract): contract is ContractPromise => !!contract);
|
||||
}
|
||||
|
||||
function ContractsTable ({ contracts: keyringContracts }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const newBlock = useCall<SignedBlockExtended>(api.derive.chain.subscribeNewBlocks);
|
||||
const [{ contractIndex, messageIndex, onCallResult }, setIndexes] = useState<Indexes>({ contractIndex: 0, messageIndex: 0 });
|
||||
const [isCallOpen, setIsCallOpen] = useState(false);
|
||||
const [contractLinks, setContractLinks] = useState<Record<string, ContractLink[]>>({});
|
||||
|
||||
const headerRef = useRef<[string?, string?, number?][]>([
|
||||
[t('contracts'), 'start'],
|
||||
[undefined, undefined, 3],
|
||||
[t('status'), 'start'],
|
||||
[]
|
||||
]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (newBlock) {
|
||||
const exts = newBlock.block.extrinsics
|
||||
.filter(({ method }) => api.tx.contracts.call.is(method))
|
||||
.map(({ args }): ContractLink | null => {
|
||||
const contractId = keyringContracts.find((a) => args[0].eq(a));
|
||||
|
||||
if (!contractId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
blockHash: newBlock.block.header.hash.toHex(),
|
||||
blockNumber: formatNumber(newBlock.block.header.number),
|
||||
contractId
|
||||
};
|
||||
})
|
||||
.filter((value): value is ContractLink => !!value);
|
||||
|
||||
exts.length && setContractLinks((links): Record<string, ContractLink[]> => {
|
||||
exts.forEach((value): void => {
|
||||
links[value.contractId] = [value].concat(links[value.contractId] || []).slice(0, 3);
|
||||
});
|
||||
|
||||
return { ...links };
|
||||
});
|
||||
}
|
||||
}, [api, keyringContracts, newBlock]);
|
||||
|
||||
const contracts = useMemo(
|
||||
() => filterContracts(api, keyringContracts),
|
||||
[api, keyringContracts]
|
||||
);
|
||||
|
||||
const _toggleCall = useCallback(
|
||||
() => setIsCallOpen((isCallOpen) => !isCallOpen),
|
||||
[]
|
||||
);
|
||||
|
||||
const _onCall = useCallback(
|
||||
(contractIndex: number, messageIndex: number, onCallResult: (messageIndex: number, result?: ContractCallOutcome) => void): void => {
|
||||
setIndexes({ contractIndex, messageIndex, onCallResult });
|
||||
setIsCallOpen(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const _setMessageIndex = useCallback(
|
||||
(messageIndex: number) => setIndexes((state) => ({ ...state, messageIndex })),
|
||||
[]
|
||||
);
|
||||
|
||||
const contract = contracts[contractIndex] || null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
empty={t('No contracts available')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{contracts.map((contract, index): React.ReactNode => (
|
||||
<Contract
|
||||
contract={contract}
|
||||
index={index}
|
||||
key={contract.address.toString()}
|
||||
links={contractLinks[contract.address.toString()]}
|
||||
onCall={_onCall}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
{isCallOpen && contract && (
|
||||
<Call
|
||||
contract={contract}
|
||||
messageIndex={messageIndex}
|
||||
onCallResult={onCallResult}
|
||||
onChangeMessage={_setMessageIndex}
|
||||
onClose={_toggleCall}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ContractsTable);
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
|
||||
import type { BlueprintSubmittableResult } from '@pezkuwi/api-contract/promise/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { BlueprintPromise } from '@pezkuwi/api-contract';
|
||||
import { Dropdown, Input, InputAddress, InputBalance, Modal, Toggle, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useFormField, useNonEmptyString } from '@pezkuwi/react-hooks';
|
||||
import { Available } from '@pezkuwi/react-query';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
import { BN_ZERO, isHex, stringify } from '@pezkuwi/util';
|
||||
import { randomAsHex } from '@pezkuwi/util-crypto';
|
||||
|
||||
import { ABI, InputMegaGas, InputName, MessageSignature, Params } from '../shared/index.js';
|
||||
import store from '../store.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import useAbi from '../useAbi.js';
|
||||
import useWeight from '../useWeight.js';
|
||||
|
||||
interface Props {
|
||||
codeHash: string;
|
||||
constructorIndex: number;
|
||||
onClose: () => void;
|
||||
setConstructorIndex: React.Dispatch<number>;
|
||||
}
|
||||
|
||||
function Deploy ({ codeHash, constructorIndex = 0, onClose, setConstructorIndex }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [initTx, setInitTx] = useState<SubmittableExtrinsic<'promise'> | null>(null);
|
||||
const [accountId, isAccountIdValid, setAccountId] = useFormField<string | null>(null);
|
||||
const [value, isValueValid, setValue] = useFormField<BN>(BN_ZERO);
|
||||
const [params, setParams] = useState<unknown[]>([]);
|
||||
const [salt, setSalt] = useState<string>(() => randomAsHex());
|
||||
const [withSalt, setWithSalt] = useState(false);
|
||||
const weight = useWeight();
|
||||
|
||||
useEffect((): void => {
|
||||
setParams([]);
|
||||
}, [constructorIndex]);
|
||||
|
||||
const code = useMemo(
|
||||
() => store.getCode(codeHash),
|
||||
[codeHash]
|
||||
);
|
||||
|
||||
const [name, isNameValid, setName] = useNonEmptyString(code?.json.name);
|
||||
const { contractAbi, errorText, isAbiError, isAbiSupplied, isAbiValid, onChangeAbi, onRemoveAbi } = useAbi([code?.json.abi, code?.contractAbi], codeHash, true);
|
||||
|
||||
const blueprint = useMemo(
|
||||
() => isAbiValid && codeHash && contractAbi
|
||||
? new BlueprintPromise(api, contractAbi, codeHash)
|
||||
: null,
|
||||
[api, codeHash, contractAbi, isAbiValid]
|
||||
);
|
||||
|
||||
const constructOptions = useMemo(
|
||||
() => contractAbi
|
||||
? contractAbi.constructors.map((c, index) => ({
|
||||
info: c.identifier,
|
||||
key: c.identifier,
|
||||
text: (
|
||||
<MessageSignature
|
||||
asConstructor
|
||||
message={c}
|
||||
/>
|
||||
),
|
||||
value: index
|
||||
}))
|
||||
: [],
|
||||
[contractAbi]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
value && setInitTx((): SubmittableExtrinsic<'promise'> | null => {
|
||||
if (blueprint && contractAbi?.constructors[constructorIndex]?.method) {
|
||||
try {
|
||||
return blueprint.tx[contractAbi.constructors[constructorIndex].method]({
|
||||
gasLimit: weight.isWeightV2 ? weight.weightV2 : weight.weight,
|
||||
salt: withSalt
|
||||
? salt
|
||||
: null,
|
||||
storageDepositLimit: null,
|
||||
value: contractAbi?.constructors[constructorIndex].isPayable ? value : undefined
|
||||
}, ...params);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}, [blueprint, contractAbi, constructorIndex, value, params, salt, weight, withSalt]);
|
||||
|
||||
const _onSuccess = useCallback(
|
||||
(result: BlueprintSubmittableResult): void => {
|
||||
if (result.contract) {
|
||||
keyring.saveContract(result.contract.address.toString(), {
|
||||
contract: {
|
||||
abi: stringify(result.contract.abi.json),
|
||||
genesisHash: api.genesisHash.toHex()
|
||||
},
|
||||
name: name || undefined,
|
||||
tags: []
|
||||
});
|
||||
|
||||
onClose && onClose();
|
||||
}
|
||||
},
|
||||
[api, name, onClose]
|
||||
);
|
||||
|
||||
const isSaltValid = !withSalt || (salt && (!salt.startsWith('0x') || isHex(salt)));
|
||||
const isValid = isNameValid && isValueValid && weight.isValid && isAccountIdValid && isSaltValid;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
header={t('Deploy a contract')}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
isInput={false}
|
||||
label={t('deployment account')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={t('transferable')}
|
||||
params={accountId}
|
||||
/>
|
||||
}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
value={accountId}
|
||||
/>
|
||||
<InputName
|
||||
isContract
|
||||
isError={!isNameValid}
|
||||
onChange={setName}
|
||||
value={name || ''}
|
||||
/>
|
||||
{!isAbiSupplied && (
|
||||
<ABI
|
||||
contractAbi={contractAbi}
|
||||
errorText={errorText}
|
||||
isError={isAbiError}
|
||||
isSupplied={isAbiSupplied}
|
||||
isValid={isAbiValid}
|
||||
onChange={onChangeAbi}
|
||||
onRemove={onRemoveAbi}
|
||||
/>
|
||||
)}
|
||||
{contractAbi && (
|
||||
<>
|
||||
<Dropdown
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{contractAbi?.constructors[constructorIndex].isPayable && (
|
||||
<InputBalance
|
||||
isError={!isValueValid}
|
||||
isZeroable
|
||||
label={t('value')}
|
||||
onChange={setValue}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
isDisabled={!withSalt}
|
||||
label={t('unique deployment salt')}
|
||||
labelExtra={
|
||||
<Toggle
|
||||
label={t('use deployment salt')}
|
||||
onChange={setWithSalt}
|
||||
value={withSalt}
|
||||
/>
|
||||
}
|
||||
onChange={setSalt}
|
||||
placeholder={t('0x prefixed hex, e.g. 0x1234 or ascii data')}
|
||||
value={withSalt ? salt : t('<none>')}
|
||||
/>
|
||||
<InputMegaGas
|
||||
weight={weight}
|
||||
/>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
extrinsic={initTx}
|
||||
icon='upload'
|
||||
isDisabled={!isValid || !initTx}
|
||||
label={t('Deploy')}
|
||||
onClick={onClose}
|
||||
onSuccess={_onSuccess}
|
||||
withSpinner
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Deploy);
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CallResult } from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button, IdentityIcon, Output, styled } from '@pezkuwi/react-components';
|
||||
import valueToText from '@pezkuwi/react-params/valueToText';
|
||||
|
||||
import MessageSignature from '../shared/MessageSignature.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onClear?: () => void;
|
||||
outcome: CallResult;
|
||||
}
|
||||
|
||||
function Outcome ({ className = '', onClear, outcome: { from, message, output, params, result, when } }: Props): React.ReactElement<Props> | null {
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<IdentityIcon value={from} />
|
||||
<Output
|
||||
className='output'
|
||||
isError={!result.isOk}
|
||||
isFull
|
||||
label={
|
||||
<MessageSignature
|
||||
message={message}
|
||||
params={params}
|
||||
/>
|
||||
}
|
||||
labelExtra={
|
||||
<span className='date-time'>
|
||||
{when.toLocaleDateString()}
|
||||
{' '}
|
||||
{when.toLocaleTimeString()}
|
||||
</span>
|
||||
}
|
||||
value={valueToText('Text', result.isOk ? output : result)}
|
||||
/>
|
||||
<Button
|
||||
icon='times'
|
||||
onClick={onClear}
|
||||
/>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
.output {
|
||||
flex: 1 1;
|
||||
margin: 0.25rem 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Outcome);
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
trigger: number;
|
||||
}
|
||||
|
||||
function Summary ({ trigger }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const accountCounter = useCall<BN>(api.query.contracts.accountCounter);
|
||||
const [numContracts, setNumContracts] = useState(0);
|
||||
const [numHashes, setNumHashes] = useState(0);
|
||||
|
||||
useEffect((): void => {
|
||||
accountCounter && api.query.contracts.contractInfoOf
|
||||
.keys()
|
||||
.then((arr) => setNumContracts(arr.length))
|
||||
.catch(console.error);
|
||||
}, [api, accountCounter]);
|
||||
|
||||
useEffect((): void => {
|
||||
(api.query.contracts.pristineCode || api.query.contracts.codeStorage)
|
||||
.keys()
|
||||
.then((arr) => setNumHashes(arr.length))
|
||||
.catch(console.error);
|
||||
}, [api, trigger]);
|
||||
|
||||
return (
|
||||
<SummaryBox>
|
||||
<section>
|
||||
<CardSummary label={t('addresses')}>
|
||||
{formatNumber(accountCounter)}
|
||||
</CardSummary>
|
||||
</section>
|
||||
<section>
|
||||
<CardSummary label={t('code hashes')}>
|
||||
{formatNumber(numHashes)}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('contracts')}>
|
||||
{formatNumber(numContracts)}
|
||||
</CardSummary>
|
||||
</section>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { ContractInfo } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { InfoForInput } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
address?: string | null;
|
||||
onChange: (isValid: boolean) => void;
|
||||
}
|
||||
|
||||
function ValidateAddr ({ address, onChange }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const contractInfo = useCall<Option<ContractInfo>>(api.query.contracts.contractInfoOf, [address]);
|
||||
const [isAddress, setIsAddress] = useState(false);
|
||||
const [isStored, setIsStored] = useState(false);
|
||||
|
||||
useEffect((): void => {
|
||||
try {
|
||||
keyring.decodeAddress(address || '');
|
||||
setIsAddress(true);
|
||||
} catch {
|
||||
setIsAddress(false);
|
||||
}
|
||||
}, [address]);
|
||||
|
||||
useEffect((): void => {
|
||||
setIsStored(!!contractInfo?.isSome);
|
||||
}, [contractInfo]);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange(isAddress && isStored);
|
||||
}, [isAddress, isStored, onChange]);
|
||||
|
||||
if (isStored || !isAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoForInput type='error'>
|
||||
{isAddress
|
||||
? t('Unable to find deployed contract code at the specified address')
|
||||
: t('The value is not in a valid address format')
|
||||
}
|
||||
</InfoForInput>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ValidateAddr);
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, styled } from '@pezkuwi/react-components';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import CodeAdd from '../Codes/Add.js';
|
||||
import Codes from '../Codes/index.js';
|
||||
import CodeUpload from '../Codes/Upload.js';
|
||||
import { useTranslation } from '../translate.js';
|
||||
import { useCodes } from '../useCodes.js';
|
||||
import { useContracts } from '../useContracts.js';
|
||||
import ContractAdd from './Add.js';
|
||||
import ContractsTable from './ContractsTable.js';
|
||||
import Deploy from './Deploy.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Contracts ({ className = '' }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { allCodes, codeTrigger } = useCodes();
|
||||
const { allContracts } = useContracts();
|
||||
const [isAddOpen, toggleAdd] = useToggle();
|
||||
const [isDeployOpen, toggleDeploy, setIsDeployOpen] = useToggle();
|
||||
const [isHashOpen, toggleHash] = useToggle();
|
||||
const [isUploadOpen, toggleUpload] = useToggle();
|
||||
const [codeHash, setCodeHash] = useState<string | undefined>();
|
||||
const [constructorIndex, setConstructorIndex] = useState(0);
|
||||
|
||||
const _onShowDeploy = useCallback(
|
||||
(codeHash: string, constructorIndex: number): void => {
|
||||
setCodeHash(codeHash || allCodes?.[0]?.json.codeHash || undefined);
|
||||
setConstructorIndex(constructorIndex);
|
||||
toggleDeploy();
|
||||
},
|
||||
[allCodes, toggleDeploy]
|
||||
);
|
||||
|
||||
const _onCloseDeploy = useCallback(
|
||||
() => setIsDeployOpen(false),
|
||||
[setIsDeployOpen]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<Summary trigger={codeTrigger} />
|
||||
<Button.Group>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Upload & deploy code')}
|
||||
onClick={toggleUpload}
|
||||
/>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Add an existing code hash')}
|
||||
onClick={toggleHash}
|
||||
/>
|
||||
<Button
|
||||
icon='plus'
|
||||
label={t('Add an existing contract')}
|
||||
onClick={toggleAdd}
|
||||
/>
|
||||
</Button.Group>
|
||||
<ContractsTable
|
||||
contracts={allContracts}
|
||||
updated={codeTrigger}
|
||||
/>
|
||||
<Codes
|
||||
onShowDeploy={_onShowDeploy}
|
||||
updated={codeTrigger}
|
||||
/>
|
||||
{codeHash && isDeployOpen && (
|
||||
<Deploy
|
||||
codeHash={codeHash}
|
||||
constructorIndex={constructorIndex}
|
||||
onClose={_onCloseDeploy}
|
||||
setConstructorIndex={setConstructorIndex}
|
||||
/>
|
||||
)}
|
||||
{isUploadOpen && (
|
||||
<CodeUpload onClose={toggleUpload} />
|
||||
)}
|
||||
{isHashOpen && (
|
||||
<CodeAdd onClose={toggleHash} />
|
||||
)}
|
||||
{isAddOpen && (
|
||||
<ContractAdd onClose={toggleAdd} />
|
||||
)}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
.ui--Table td > article {
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Contracts);
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AbiMessage, ContractCallOutcome } from '@pezkuwi/api-contract/types';
|
||||
|
||||
export interface CallResult extends ContractCallOutcome {
|
||||
from: string;
|
||||
message: AbiMessage;
|
||||
params: unknown[];
|
||||
when: Date;
|
||||
}
|
||||
|
||||
export interface ContractLink {
|
||||
blockHash: string;
|
||||
blockNumber: string;
|
||||
contractId: string;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DropdownItemProps } from 'semantic-ui-react';
|
||||
import type { ApiPromise } from '@pezkuwi/api';
|
||||
import type { AbiMessage } from '@pezkuwi/api-contract/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ContractPromise } from '@pezkuwi/api-contract';
|
||||
import { getContractAbi } from '@pezkuwi/react-components/util';
|
||||
|
||||
import MessageSignature from '../shared/MessageSignature.js';
|
||||
|
||||
export function findCallMethod (callContract: ContractPromise | null, callMethodIndex = 0): AbiMessage | null {
|
||||
return callContract?.abi.messages[callMethodIndex] || null;
|
||||
}
|
||||
|
||||
export function getContractMethodFn (callContract: ContractPromise | null, callMethodIndex: number | null): AbiMessage | null {
|
||||
const fn = callMethodIndex !== null && callContract?.abi?.messages[callMethodIndex];
|
||||
|
||||
return fn || null;
|
||||
}
|
||||
|
||||
export function getContractForAddress (api: ApiPromise, address: string | null): ContractPromise | null {
|
||||
if (!address) {
|
||||
return null;
|
||||
} else {
|
||||
const abi = getContractAbi(address);
|
||||
|
||||
return abi
|
||||
? new ContractPromise(api, abi, address)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCallMessageOptions (callContract: ContractPromise | null): DropdownItemProps[] {
|
||||
return callContract?.abi.messages.map((m, index) => ({
|
||||
key: m.identifier,
|
||||
text: (
|
||||
<MessageSignature message={m} />
|
||||
),
|
||||
value: index
|
||||
})) || [];
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-accounts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CodeStored } from './types.js';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, Modal } from '@pezkuwi/react-components';
|
||||
|
||||
import CodeRow from './shared/CodeRow.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
interface Props {
|
||||
code: CodeStored;
|
||||
onClose: () => void;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
function RemoveABI ({ code, onClose, onRemove }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _onRemove = useCallback(
|
||||
(): void => {
|
||||
onClose && onClose();
|
||||
onRemove();
|
||||
},
|
||||
[onClose, onRemove]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='app--accounts-Modal'
|
||||
header={t('Confirm ABI removal')}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal.Content>
|
||||
<CodeRow
|
||||
code={code}
|
||||
isInline
|
||||
>
|
||||
<p>{t('You are about to remove this code\'s ABI. Once completed, should you need to access it again, you will have to manually re-upload it.')}</p>
|
||||
<p>{t('This operation does not impact the associated on-chain code or any of its contracts.')}</p>
|
||||
</CodeRow>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
icon='trash'
|
||||
label={t('Remove')}
|
||||
onClick={_onRemove}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(RemoveABI);
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-staking authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const ENDOWMENT = 1000;
|
||||
export const GAS_LIMIT = '100000000000';
|
||||
export const CONTRACT_NULL = {
|
||||
abi: null,
|
||||
address: null
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AppProps as Props } from '@pezkuwi/react-components/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Tabs } from '@pezkuwi/react-components';
|
||||
|
||||
import Contracts from './Contracts/index.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
|
||||
function ContractsApp ({ basePath, className = '' }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const itemsRef = useRef([
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'contracts',
|
||||
text: t('Contracts')
|
||||
}
|
||||
]);
|
||||
|
||||
return (
|
||||
<main className={`${className} contracts--App`}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
items={itemsRef.current}
|
||||
/>
|
||||
<Contracts />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ContractsApp);
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Abi } from '@pezkuwi/api-contract';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { IconLink, InputFile, Labelled } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Messages from './Messages.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
contractAbi?: Abi | null;
|
||||
errorText?: string | null;
|
||||
isDisabled?: boolean;
|
||||
isError?: boolean;
|
||||
isFull?: boolean;
|
||||
isRequired?: boolean;
|
||||
isValid?: boolean;
|
||||
isSupplied?: boolean;
|
||||
label?: React.ReactNode;
|
||||
onChange: (u8a: Uint8Array, name: string) => void;
|
||||
onRemove?: () => void;
|
||||
onRemoved?: () => void;
|
||||
onSelect?: () => void;
|
||||
onSelectConstructor?: (index?: number) => void;
|
||||
withConstructors?: boolean;
|
||||
withLabel?: boolean;
|
||||
withMessages?: boolean;
|
||||
withWasm?: boolean;
|
||||
}
|
||||
|
||||
const NOOP = (): void => undefined;
|
||||
|
||||
function ABI ({ className, contractAbi, errorText, isDisabled, isError, isFull, isValid, label, onChange, onRemove = NOOP, onSelectConstructor, withConstructors = true, withLabel = true, withMessages = true, withWasm }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (contractAbi && isValid)
|
||||
? (
|
||||
<Labelled
|
||||
className={className}
|
||||
label={label || t('contract ABI')}
|
||||
labelExtra={onRemove && (
|
||||
<IconLink
|
||||
icon='trash'
|
||||
label={t('Remove ABI')}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
)}
|
||||
withLabel={withLabel}
|
||||
>
|
||||
<Messages
|
||||
contractAbi={contractAbi}
|
||||
isLabelled={withLabel}
|
||||
onSelectConstructor={onSelectConstructor}
|
||||
withConstructors={withConstructors}
|
||||
withMessages={withMessages}
|
||||
withWasm={withWasm}
|
||||
/>
|
||||
</Labelled>
|
||||
)
|
||||
: (
|
||||
<div className={className}>
|
||||
<InputFile
|
||||
isDisabled={isDisabled}
|
||||
isError={isError}
|
||||
isFull={isFull}
|
||||
label={label || t('contract ABI')}
|
||||
onChange={onChange}
|
||||
placeholder={errorText || t('click to select or drag and drop a JSON file')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ABI);
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CodeStored } from '../types.js';
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Icon, styled } from '@pezkuwi/react-components';
|
||||
import Row from '@pezkuwi/react-components/Row';
|
||||
|
||||
import contracts from '../store.js';
|
||||
|
||||
interface Props {
|
||||
buttons?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
code: CodeStored;
|
||||
isInline?: boolean;
|
||||
withTags?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_HASH = '0x';
|
||||
const DEFAULT_NAME = '<unknown>';
|
||||
|
||||
function CodeRow ({ buttons, children, className, code: { json }, isInline, withTags }: Props): React.ReactElement<Props> {
|
||||
const [name, setName] = useState(json.name || DEFAULT_NAME);
|
||||
const [tags, setTags] = useState(json.tags || []);
|
||||
const [codeHash, setCodeHash] = useState(json.codeHash || DEFAULT_HASH);
|
||||
|
||||
useEffect((): void => {
|
||||
setName(json.name || DEFAULT_NAME);
|
||||
setTags(json.tags || []);
|
||||
setCodeHash(json.codeHash || DEFAULT_HASH);
|
||||
}, [json]);
|
||||
|
||||
const _onSaveName = useCallback(
|
||||
(): void => {
|
||||
const trimmedName = name.trim();
|
||||
|
||||
if (trimmedName && codeHash) {
|
||||
contracts.saveCode(codeHash, { name });
|
||||
}
|
||||
},
|
||||
[codeHash, name]
|
||||
);
|
||||
|
||||
const _onSaveTags = useCallback(
|
||||
(): void => {
|
||||
codeHash && contracts.saveCode(codeHash, { tags });
|
||||
},
|
||||
[codeHash, tags]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRow
|
||||
buttons={buttons}
|
||||
className={className}
|
||||
icon={
|
||||
<div className='ui--CodeRow-icon'>
|
||||
<Icon icon='code' />
|
||||
</div>
|
||||
}
|
||||
isInline={isInline}
|
||||
name={name}
|
||||
onChangeName={setName}
|
||||
onChangeTags={setTags}
|
||||
onSaveName={_onSaveName}
|
||||
onSaveTags={_onSaveTags}
|
||||
tags={withTags ? tags : undefined}
|
||||
>
|
||||
{children}
|
||||
</StyledRow>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledRow = styled(Row)`
|
||||
.ui--CodeRow-icon {
|
||||
margin-right: -0.5em;
|
||||
background: #eee;
|
||||
border-radius: 50%;
|
||||
color: #666;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(CodeRow);
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { WeightV2 } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { UseWeight } from '../types.js';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { InputNumber, Toggle } from '@pezkuwi/react-components';
|
||||
import { BN_MILLION, BN_ONE, BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
estimatedWeight?: BN | null;
|
||||
estimatedWeightV2?: WeightV2 | null;
|
||||
isCall?: boolean;
|
||||
weight: UseWeight;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const estimatedMg = useMemo(
|
||||
() => estimatedWeight
|
||||
? estimatedWeight.div(BN_MILLION).iadd(BN_ONE)
|
||||
: null,
|
||||
[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]);
|
||||
|
||||
const isDisabled = !!estimatedMg && withEstimate;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{isWeightV2
|
||||
? <>
|
||||
<InputNumber
|
||||
defaultValue={estimatedMgRefTime && isDisabled ? estimatedMgRefTime.toString() : undefined}
|
||||
isDisabled={isDisabled}
|
||||
isError={!isValid}
|
||||
isZeroable={isCall}
|
||||
label={
|
||||
estimatedMgRefTime && (isCall ? !withEstimate : true)
|
||||
? t('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) && (
|
||||
<Toggle
|
||||
label={
|
||||
isCall
|
||||
? t('max read gas')
|
||||
: t('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('max ProofSize allowed ({{estimatedProofSize}} estimated)', { replace: { estimatedProofSize: estimatedProofSize.toString() } })
|
||||
: t('max ProofSize allowed')
|
||||
}
|
||||
onChange={isDisabled ? undefined : setProofSize}
|
||||
value={isDisabled ? undefined : ((isCall && withEstimate) ? BN_ZERO : proofSize)}
|
||||
/>
|
||||
<div className='contracts--Input-meter'>
|
||||
{t('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '}
|
||||
{t('{{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('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) && (
|
||||
<Toggle
|
||||
label={
|
||||
isCall
|
||||
? t('max read gas')
|
||||
: t('use estimated gas')
|
||||
}
|
||||
onChange={setWithEstimate}
|
||||
value={withEstimate}
|
||||
/>
|
||||
)}
|
||||
</InputNumber>
|
||||
<div className='contracts--Input-meter'>
|
||||
{t('{{executionTime}}s execution time', { replace: { executionTime: executionTime.toFixed(3) } })}{', '}
|
||||
{t('{{percentage}}% of block weight', { replace: { percentage: percentage.toFixed(2) } })}
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(InputMegaGas);
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Input } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isBusy?: boolean;
|
||||
isContract?: boolean;
|
||||
isError?: boolean;
|
||||
isDisabled?: boolean;
|
||||
onChange: (_: string) => void;
|
||||
onEnter?: () => void;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
function InputName ({ className, isBusy, isContract, isError, onChange, onEnter, value = '' }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={className}
|
||||
isDisabled={isBusy}
|
||||
isError={isError}
|
||||
label={t(
|
||||
isContract
|
||||
? 'contract name'
|
||||
: 'code bundle name'
|
||||
)}
|
||||
onChange={onChange}
|
||||
onEnter={onEnter}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(InputName);
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AbiConstructor, ContractCallOutcome } from '@pezkuwi/api-contract/types';
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { Button, Output, styled } from '@pezkuwi/react-components';
|
||||
import valueToText from '@pezkuwi/react-params/valueToText';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import MessageSignature from './MessageSignature.js';
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
index: number;
|
||||
lastResult?: ContractCallOutcome;
|
||||
message: AbiConstructor;
|
||||
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]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv
|
||||
className={`${className} ${!onSelect ? 'exempt-hover' : ''} ${message.isConstructor ? 'constructor' : ''}`}
|
||||
key={`${message.identifier}-${index}`}
|
||||
>
|
||||
{onSelect && (
|
||||
message.isConstructor
|
||||
? (
|
||||
<Button
|
||||
className='accessory'
|
||||
icon='upload'
|
||||
label={t('deploy')}
|
||||
onClick={_onSelect}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
className='accessory'
|
||||
icon='play'
|
||||
isDisabled={message.isMutating ? false : (!message.args.length && lastResult?.result.isOk)}
|
||||
label={message.isMutating ? t('exec') : t('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('No documentation provided')} </i>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{lastResult && lastResult.result.isOk && lastResult.output && (
|
||||
<Output
|
||||
className='result'
|
||||
isFull
|
||||
label={t('current value')}
|
||||
>
|
||||
{valueToText('Text', lastResult.output)}
|
||||
</Output>
|
||||
)}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
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: var(--font-size-small);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
min-width: 15rem;
|
||||
}
|
||||
|
||||
&+& {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Message);
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AbiMessage } from '@pezkuwi/api-contract/types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, styled, Tooltip } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { encodeTypeDef } from '@pezkuwi/types/create';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
const MAX_PARAM_LENGTH = 20;
|
||||
|
||||
export interface Props {
|
||||
asConstructor?: boolean;
|
||||
className?: string;
|
||||
message: AbiMessage;
|
||||
params?: unknown[];
|
||||
withTooltip?: boolean;
|
||||
}
|
||||
|
||||
function truncate (param: string): string {
|
||||
return param.length > MAX_PARAM_LENGTH
|
||||
? `${param.substring(0, MAX_PARAM_LENGTH / 2)}…${param.substring(param.length - MAX_PARAM_LENGTH / 2)}`
|
||||
: param;
|
||||
}
|
||||
|
||||
function MessageSignature ({ className, message: { args, isConstructor, isMutating, method, returnType }, params = [], withTooltip = false }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<span className='ui--MessageSignature-name'>{method}</span>
|
||||
{' '}({args.map(({ name, type }, index): React.ReactNode => {
|
||||
return (
|
||||
<React.Fragment key={`${name}-args-${index}`}>
|
||||
{name}:
|
||||
{' '}
|
||||
<span className='ui--MessageSignature-type'>
|
||||
{params?.[index]
|
||||
? <b>{truncate((params as string[])[index].toString())}</b>
|
||||
: encodeTypeDef(api.registry, type)
|
||||
}
|
||||
</span>
|
||||
{index < (args.length) - 1 && ', '}
|
||||
</React.Fragment>
|
||||
);
|
||||
})})
|
||||
{(!isConstructor && returnType) && (
|
||||
<>
|
||||
:
|
||||
{' '}
|
||||
<span className='ui--MessageSignature-returnType'>
|
||||
{encodeTypeDef(api.registry, returnType)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{isMutating && (
|
||||
<>
|
||||
<Icon
|
||||
className='ui--MessageSignature-mutates'
|
||||
icon='database'
|
||||
tooltip={`mutates-${method}`}
|
||||
/>
|
||||
{withTooltip && (
|
||||
<Tooltip
|
||||
text={t('Mutates contract state')}
|
||||
trigger={`mutates-${method}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
font: var(--font-mono);
|
||||
font-weight: var(--font-weight-normal);
|
||||
flex-grow: 1;
|
||||
|
||||
.ui--MessageSignature-mutates {
|
||||
color: #ff8600;
|
||||
margin-left: 0.5rem;
|
||||
opacity: var(--opacity-light);
|
||||
}
|
||||
|
||||
.ui--MessageSignature-name {
|
||||
color: #2f8ddb;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.ui--MessageSignature-type {
|
||||
color: #21a2b2;
|
||||
}
|
||||
|
||||
.ui--MessageSignature-returnType {
|
||||
color: #ff8600;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(MessageSignature);
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Abi, ContractPromise } from '@pezkuwi/api-contract';
|
||||
import type { AbiMessage, ContractCallOutcome } from '@pezkuwi/api-contract/types';
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { ContractInfo } from '@pezkuwi/types/interfaces';
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Expander, styled } from '@pezkuwi/react-components';
|
||||
import { useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Message from './Message.js';
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
contract?: ContractPromise;
|
||||
contractAbi: Abi;
|
||||
isLabelled?: boolean;
|
||||
isWatching?: boolean;
|
||||
trigger?: number;
|
||||
onSelect?: (messageIndex: number, resultCb: (messageIndex: number, result?: ContractCallOutcome) => void) => void;
|
||||
onSelectConstructor?: (constructorIndex: number) => void;
|
||||
withConstructors?: boolean;
|
||||
withMessages?: boolean;
|
||||
withWasm?: boolean;
|
||||
}
|
||||
|
||||
const READ_ADDR = '0x'.padEnd(66, '0');
|
||||
|
||||
function sortMessages (messages: AbiMessage[]): [AbiMessage, number][] {
|
||||
return messages
|
||||
.map((m, index): [AbiMessage, number] => [m, index])
|
||||
.sort((a, b) => a[0].identifier.localeCompare(b[0].identifier))
|
||||
.sort((a, b) => a[0].isMutating === b[0].isMutating
|
||||
? 0
|
||||
: a[0].isMutating
|
||||
? -1
|
||||
: 1
|
||||
);
|
||||
}
|
||||
|
||||
function Messages ({ className = '', contract, contractAbi: { constructors, info: { source }, messages }, isLabelled, isWatching, onSelect, onSelectConstructor, trigger, withConstructors, withMessages, withWasm }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const optInfo = useCall<Option<ContractInfo>>(contract && api.query.contracts.contractInfoOf, [contract?.address]);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [lastResults, setLastResults] = useState<(ContractCallOutcome | undefined)[]>([]);
|
||||
|
||||
const _onExpander = useCallback(
|
||||
(isOpen: boolean): void => {
|
||||
isWatching && setIsUpdating(isOpen);
|
||||
},
|
||||
[isWatching]
|
||||
);
|
||||
|
||||
const _onRefresh = useCallback(
|
||||
(): void => {
|
||||
optInfo && contract &&
|
||||
Promise
|
||||
.all(
|
||||
messages.map((m) =>
|
||||
m.isMutating || m.args.length !== 0
|
||||
? Promise.resolve(undefined)
|
||||
: contract
|
||||
.query[m.method](READ_ADDR, { gasLimit: -1, value: 0 })
|
||||
.catch((e: Error) => console.error(`contract.query.${m.method}:: ${e.message}`))
|
||||
.then(() => undefined)
|
||||
)
|
||||
)
|
||||
.then(setLastResults)
|
||||
.catch(console.error);
|
||||
},
|
||||
[contract, messages, optInfo]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
(isUpdating || trigger) && optInfo && contract && _onRefresh();
|
||||
}, [_onRefresh, contract, isUpdating, optInfo, trigger]);
|
||||
|
||||
const _setMessageResult = useCallback(
|
||||
(_messageIndex: number, _result?: ContractCallOutcome): void => {
|
||||
// ignore
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const _onSelect = useCallback(
|
||||
(index: number) => onSelect && onSelect(index, _setMessageResult),
|
||||
[_setMessageResult, onSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={`${className} ui--Messages ${isLabelled ? 'isLabelled' : ''}`}>
|
||||
{withConstructors && (
|
||||
<Expander summary={t('Constructors ({{count}})', { replace: { count: constructors.length } })}>
|
||||
{sortMessages(constructors).map(([message, index]) => (
|
||||
<Message
|
||||
index={index}
|
||||
key={index}
|
||||
message={message}
|
||||
onSelect={onSelectConstructor}
|
||||
/>
|
||||
))}
|
||||
</Expander>
|
||||
)}
|
||||
{withMessages && (
|
||||
<Expander
|
||||
onClick={_onExpander}
|
||||
summary={t('Messages ({{count}})', { replace: { count: messages.length } })}
|
||||
>
|
||||
{sortMessages(messages).map(([message, index]) => (
|
||||
<Message
|
||||
index={index}
|
||||
key={index}
|
||||
lastResult={lastResults[index]}
|
||||
message={message}
|
||||
onSelect={_onSelect}
|
||||
/>
|
||||
))}
|
||||
</Expander>
|
||||
)}
|
||||
{withWasm && source.wasm.length !== 0 && (
|
||||
<div>{t('{{size}} WASM bytes', { replace: { size: formatNumber(source.wasm.length) } })}</div>
|
||||
)}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
padding-bottom: 0.75rem !important;
|
||||
|
||||
&.isLabelled {
|
||||
background: var(--bg-input);
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border-input);
|
||||
border-radius: .28571429rem;
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Messages);
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { RawParams } from '@pezkuwi/react-params/types';
|
||||
import type { Registry, TypeDef } from '@pezkuwi/types/types';
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import UIParams from '@pezkuwi/react-params';
|
||||
|
||||
interface Props {
|
||||
isDisabled?: boolean;
|
||||
params?: ParamDef[] | null | '';
|
||||
onChange: (values: unknown[]) => void;
|
||||
onEnter?: () => void;
|
||||
registry: Registry;
|
||||
}
|
||||
|
||||
interface ParamDef {
|
||||
name: string;
|
||||
type: TypeDef;
|
||||
}
|
||||
|
||||
function Params ({ isDisabled, onChange, onEnter, params: propParams, registry }: Props): React.ReactElement<Props> | null {
|
||||
const [params, setParams] = useState<ParamDef[]>([]);
|
||||
|
||||
useEffect((): void => {
|
||||
propParams && setParams(propParams);
|
||||
}, [propParams]);
|
||||
|
||||
const _onChange = useCallback(
|
||||
(values: RawParams) => onChange(values.map(({ value }) => value)),
|
||||
[onChange]
|
||||
);
|
||||
|
||||
if (!params.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UIParams
|
||||
isDisabled={isDisabled}
|
||||
onChange={_onChange}
|
||||
onEnter={onEnter}
|
||||
params={params}
|
||||
registry={registry}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Params);
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { default as ABI } from './ABI.js';
|
||||
export { default as CodeRow } from './CodeRow.js';
|
||||
export { default as InputMegaGas } from './InputMegaGas.js';
|
||||
export { default as InputName } from './InputName.js';
|
||||
export { default as Messages } from './Messages.js';
|
||||
export { default as MessageSignature } from './MessageSignature.js';
|
||||
export { default as Params } from './Params.js';
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Hash } from '@pezkuwi/types/interfaces';
|
||||
import type { CodeJson, CodeStored } from './types.js';
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import store from 'store';
|
||||
|
||||
import { Abi } from '@pezkuwi/api-contract';
|
||||
import { statics } from '@pezkuwi/react-api/statics';
|
||||
import { isString } from '@pezkuwi/util';
|
||||
|
||||
const KEY_CODE = 'code:';
|
||||
|
||||
class Store extends EventEmitter {
|
||||
#allCode: Record<string, CodeStored> = {};
|
||||
|
||||
public get hasCode (): boolean {
|
||||
return Object.keys(this.#allCode).length !== 0;
|
||||
}
|
||||
|
||||
public getAllCode (): CodeStored[] {
|
||||
return Object.values(this.#allCode);
|
||||
}
|
||||
|
||||
public getCode (codeHash: string): CodeStored | undefined {
|
||||
return this.#allCode[codeHash];
|
||||
}
|
||||
|
||||
public saveCode (_codeHash: string | Hash, partial: Partial<CodeJson>): void {
|
||||
const codeHash = (isString(_codeHash) ? statics.api.registry.createType('Hash', _codeHash) : _codeHash).toHex();
|
||||
const existing = this.getCode(codeHash);
|
||||
const json = {
|
||||
...(existing ? existing.json : {}),
|
||||
...partial,
|
||||
codeHash,
|
||||
genesisHash: statics.api.genesisHash.toHex(),
|
||||
whenCreated: existing?.json.whenCreated || Date.now()
|
||||
};
|
||||
const key = `${KEY_CODE}${json.codeHash}`;
|
||||
|
||||
store.set(key, json);
|
||||
this.addCode(key, json as CodeJson);
|
||||
}
|
||||
|
||||
public forgetCode (codeHash: string): void {
|
||||
this.removeCode(`${KEY_CODE}${codeHash}`, codeHash);
|
||||
}
|
||||
|
||||
public loadAll (onLoaded?: () => void): void {
|
||||
try {
|
||||
const genesisHash = statics.api.genesisHash.toHex();
|
||||
|
||||
store.each((json: CodeJson, key: string): void => {
|
||||
if (json && json.genesisHash === genesisHash && key.startsWith(KEY_CODE)) {
|
||||
this.addCode(key, json);
|
||||
}
|
||||
});
|
||||
|
||||
onLoaded && onLoaded();
|
||||
} catch (error) {
|
||||
console.error('Unable to load code', error);
|
||||
}
|
||||
}
|
||||
|
||||
private addCode (key: string, json: CodeJson): void {
|
||||
try {
|
||||
this.#allCode[json.codeHash] = {
|
||||
contractAbi: json.abi
|
||||
? new Abi(json.abi, statics.api.registry.getChainProperties())
|
||||
: undefined,
|
||||
json
|
||||
};
|
||||
|
||||
this.emit('new-code');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.removeCode(key, json.codeHash);
|
||||
}
|
||||
}
|
||||
|
||||
private removeCode (key: string, codeHash: string): void {
|
||||
try {
|
||||
delete this.#allCode[codeHash];
|
||||
store.remove(key);
|
||||
this.emit('removed-code');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Store();
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useTranslation as useTranslationBase } from 'react-i18next';
|
||||
|
||||
export function useTranslation (): { t: (key: string, options?: { replace: Record<string, unknown> }) => string } {
|
||||
return useTranslationBase('app-contracts');
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-contracts authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type React from 'react';
|
||||
import type { Abi } from '@pezkuwi/api-contract';
|
||||
import type { WeightV2 } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export interface CodeJson {
|
||||
abi?: string | null;
|
||||
codeHash: string;
|
||||
name: string;
|
||||
genesisHash: string;
|
||||
tags: string[];
|
||||
whenCreated: number;
|
||||
}
|
||||
|
||||
export interface CodeStored {
|
||||
json: CodeJson;
|
||||
contractAbi?: Abi;
|
||||
}
|
||||
|
||||
export interface ContractJsonOld {
|
||||
genesisHash: string;
|
||||
abi: string;
|
||||
address: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Abi } from '@pezkuwi/api-contract';
|
||||
import { statics } from '@pezkuwi/react-api/statics';
|
||||
import { createNamedHook } from '@pezkuwi/react-hooks';
|
||||
import { u8aToString } from '@pezkuwi/util';
|
||||
|
||||
import store from './store.js';
|
||||
|
||||
interface AbiState {
|
||||
abi: string | null;
|
||||
abiName: string | null;
|
||||
contractAbi: Abi | null;
|
||||
errorText: string | null;
|
||||
isAbiError: boolean;
|
||||
isAbiValid: boolean;
|
||||
isAbiSupplied: boolean;
|
||||
}
|
||||
|
||||
interface UseAbi extends AbiState {
|
||||
onChangeAbi: (u8a: Uint8Array, name: string) => void;
|
||||
onRemoveAbi: () => void;
|
||||
}
|
||||
|
||||
function fromInitial (initialValue: [string | null | undefined, Abi | null | undefined], isRequired: boolean): AbiState {
|
||||
return {
|
||||
abi: initialValue[0] || null,
|
||||
abiName: null,
|
||||
contractAbi: initialValue[1] || null,
|
||||
errorText: null,
|
||||
isAbiError: false,
|
||||
isAbiSupplied: !!initialValue[1],
|
||||
isAbiValid: !isRequired || !!initialValue[1]
|
||||
};
|
||||
}
|
||||
|
||||
const EMPTY: AbiState = {
|
||||
abi: null,
|
||||
abiName: null,
|
||||
contractAbi: null,
|
||||
errorText: null,
|
||||
isAbiError: false,
|
||||
isAbiSupplied: false,
|
||||
isAbiValid: false
|
||||
};
|
||||
|
||||
function useAbiImpl (initialValue: [string | null | undefined, Abi | null | undefined] = [null, null], codeHash: string | null = null, isRequired = false): UseAbi {
|
||||
const [state, setAbi] = useState<AbiState>(() => fromInitial(initialValue, isRequired));
|
||||
|
||||
useEffect(
|
||||
() => setAbi((state) =>
|
||||
initialValue[0] && state.abi !== initialValue[0]
|
||||
? fromInitial(initialValue, isRequired)
|
||||
: state
|
||||
),
|
||||
[initialValue, isRequired]
|
||||
);
|
||||
|
||||
const onChangeAbi = useCallback(
|
||||
(u8a: Uint8Array, name: string): void => {
|
||||
const json = u8aToString(u8a);
|
||||
|
||||
try {
|
||||
setAbi({
|
||||
abi: json,
|
||||
abiName: name.replace('.contract', '').replace('.json', '').replace('_', ' '),
|
||||
contractAbi: new Abi(json, statics.api.registry.getChainProperties()),
|
||||
errorText: null,
|
||||
isAbiError: false,
|
||||
isAbiSupplied: true,
|
||||
isAbiValid: true
|
||||
});
|
||||
|
||||
codeHash && store.saveCode(codeHash, { abi: json });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
setAbi({ ...EMPTY, errorText: (error as Error).message });
|
||||
}
|
||||
},
|
||||
[codeHash]
|
||||
);
|
||||
|
||||
const onRemoveAbi = useCallback(
|
||||
(): void => {
|
||||
setAbi(EMPTY);
|
||||
|
||||
codeHash && store.saveCode(codeHash, { abi: null });
|
||||
},
|
||||
[codeHash]
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
onChangeAbi,
|
||||
onRemoveAbi
|
||||
};
|
||||
}
|
||||
|
||||
export default createNamedHook('useAbi', useAbiImpl);
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { CodeStored } from './types.js';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useIsMountedRef } from '@pezkuwi/react-hooks';
|
||||
|
||||
import store from './store.js';
|
||||
|
||||
interface UseCodes {
|
||||
allCodes: CodeStored[];
|
||||
codeTrigger: number;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: UseCodes = { allCodes: [], codeTrigger: Date.now() };
|
||||
|
||||
function useCodesImpl (): UseCodes {
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [state, setState] = useState<UseCodes>(DEFAULT_STATE);
|
||||
|
||||
useEffect(
|
||||
(): void => {
|
||||
const triggerUpdate = (): void => {
|
||||
mountedRef.current && setState({
|
||||
allCodes: store.getAllCode(),
|
||||
codeTrigger: Date.now()
|
||||
});
|
||||
};
|
||||
|
||||
store.on('new-code', triggerUpdate);
|
||||
store.on('removed-code', triggerUpdate);
|
||||
store.loadAll(triggerUpdate);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const useCodes = createNamedHook('useCodes', useCodesImpl);
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useIsMountedRef } from '@pezkuwi/react-hooks';
|
||||
import { keyring } from '@pezkuwi/ui-keyring';
|
||||
import { nextTick } from '@pezkuwi/util';
|
||||
|
||||
interface UseContracts {
|
||||
allContracts: string[];
|
||||
hasContracts: boolean;
|
||||
isContract: (address: string) => boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: UseContracts = {
|
||||
allContracts: [],
|
||||
hasContracts: false,
|
||||
isContract: () => false
|
||||
};
|
||||
|
||||
function useContractsImpl (): UseContracts {
|
||||
const mountedRef = useIsMountedRef();
|
||||
const [state, setState] = useState<UseContracts>(DEFAULT_STATE);
|
||||
|
||||
useEffect((): () => void => {
|
||||
const subscription = keyring.contracts.subject.subscribe((contracts): void => {
|
||||
if (mountedRef.current) {
|
||||
const allContracts = contracts ? Object.keys(contracts) : [];
|
||||
const hasContracts = allContracts.length !== 0;
|
||||
const isContract = (address: string) => allContracts.includes(address);
|
||||
|
||||
setState({ allContracts, hasContracts, isContract });
|
||||
}
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
nextTick(() => subscription.unsubscribe());
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const useContracts = createNamedHook('useContracts', useContractsImpl);
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Weight, WeightV2 } from '@pezkuwi/types/interfaces';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { UseWeight } from './types.js';
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useApi, useBlockInterval } from '@pezkuwi/react-hooks';
|
||||
import { convertWeight } from '@pezkuwi/react-hooks/useWeight';
|
||||
import { BN_MILLION, BN_ONE, BN_TEN, BN_ZERO } from '@pezkuwi/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
|
||||
? api.consts.system.blockWeights.maxBlock
|
||||
: 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(
|
||||
(value?: BN | undefined) =>
|
||||
_setMegaGas(value || convertWeight(
|
||||
api.consts.system.blockWeights
|
||||
? api.consts.system.blockWeights.maxBlock
|
||||
: api.consts.system.maximumBlockWeight as Weight
|
||||
).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) {
|
||||
weight = megaGas.mul(BN_MILLION);
|
||||
executionTime = weight.mul(blockTime).div(convertWeight(
|
||||
api.consts.system.blockWeights
|
||||
? api.consts.system.blockWeights.maxBlock
|
||||
: api.consts.system.maximumBlockWeight as Weight
|
||||
).v1Weight).toNumber();
|
||||
percentage = (executionTime / blockTime.toNumber()) * 100;
|
||||
|
||||
// execution is 2s of 6s blocks, i.e. 1/3
|
||||
executionTime = executionTime / 3000;
|
||||
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,
|
||||
setMegaRefTime,
|
||||
setProofSize,
|
||||
weight,
|
||||
weightV2
|
||||
};
|
||||
}, [api, blockTime, isEmpty, isWeightV2, megaGas, megaRefTime, proofSize, setIsEmpty, setMegaGas, setMegaRefTime, setProofSize]);
|
||||
}
|
||||
|
||||
export default createNamedHook('useWeight', useWeightImpl);
|
||||
Reference in New Issue
Block a user