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:
2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 changed files with 329019 additions and 0 deletions
+85
View File
@@ -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);
+130
View File
@@ -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
})) || [];
}
+56
View File
@@ -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);
+9
View File
@@ -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
};
+35
View File
@@ -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>&nbsp;{t('No documentation provided')}&nbsp;</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';
+94
View File
@@ -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();
+8
View File
@@ -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');
}
+45
View File
@@ -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;
}
+103
View File
@@ -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);
+43
View File
@@ -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);
+125
View File
@@ -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);