mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-08-01 12:55:48 +00:00
feat: initial Pezkuwi Apps rebrand from polkadot-apps
Rebranded terminology: - Polkadot → Pezkuwi - Kusama → Dicle - Westend → Zagros - Rococo → PezkuwiChain - Substrate → Bizinikiwi - parachain → teyrchain Custom logos with Kurdistan brand colors (#e6007a → #86e62a): - bizinikiwi-hexagon.svg - sora-bizinikiwi.svg - hezscanner.svg - heztreasury.svg - pezkuwiscan.svg - pezkuwistats.svg - pezkuwiassembly.svg - pezkuwiholic.svg
This commit is contained in:
@@ -0,0 +1,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
|
||||
})) || [];
|
||||
}
|
||||
Reference in New Issue
Block a user