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
@@ -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
})) || [];
}