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,125 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@pezkuwi/api';
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { BN } from '@pezkuwi/util';
import type { HexString } from '@pezkuwi/util/types';
import type { HashState } from './types.js';
import React, { useCallback, useState } from 'react';
import { InputBalance, Modal, Static } from '@pezkuwi/react-components';
import { useApi } from '@pezkuwi/react-hooks';
import { Extrinsic } from '@pezkuwi/react-params';
import { BN_ZERO } from '@pezkuwi/util';
import { blake2AsHex } from '@pezkuwi/util-crypto';
import { useTranslation } from '../../translate.js';
interface Props {
className?: string;
onChange: (state: HashState) => void;
}
const EMPTY_HASH = blake2AsHex('');
export const EMPTY_PROPOSAL: HashState = {
encodedHash: EMPTY_HASH,
encodedLength: 0,
encodedProposal: null,
notePreimageTx: null,
storageFee: BN_ZERO
};
function getState (api: ApiPromise, proposal?: SubmittableExtrinsic<'promise'>): HashState {
let encodedHash = EMPTY_HASH;
let encodedProposal: HexString | null = null;
let encodedLength = 0;
let notePreimageTx: SubmittableExtrinsic<'promise'> | null = null;
let storageFee = BN_ZERO;
if (proposal) {
encodedProposal = proposal.method.toHex();
encodedLength = Math.ceil((encodedProposal.length - 2) / 2);
encodedHash = blake2AsHex(encodedProposal);
notePreimageTx = api.tx.preimage.notePreimage(encodedProposal);
// we currently don't have a constant exposed, however match to Bizinikiwi
storageFee = ((api.consts.preimage?.baseDeposit || BN_ZERO) as unknown as BN).add(
((api.consts.preimage?.byteDeposit || BN_ZERO) as unknown as BN).muln(encodedLength)
);
}
return {
encodedHash,
encodedLength,
encodedProposal,
notePreimageTx,
storageFee
};
}
function Partial ({ className, onChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api, apiDefaultTxSudo } = useApi();
const [{ encodedHash, encodedLength, storageFee }, setState] = useState<HashState>(EMPTY_PROPOSAL);
const changeState = useCallback(
(state: HashState): void => {
setState(state);
onChange(state);
},
[onChange]
);
const setProposal = useCallback(
(proposal?: SubmittableExtrinsic<'promise'>) =>
changeState(getState(api, proposal)),
[api, changeState]
);
return (
<>
<Modal.Columns
className={className}
hint={
<>
<p>{t('The image (proposal) will be stored on-chain against the hash of the contents.')}</p>
<p>{t('When submitting a proposal the hash needs to be known. Proposals can be submitted with hash-only, but upon dispatch the preimage needs to be available.')}</p>
</>
}
>
<Extrinsic
defaultValue={apiDefaultTxSudo}
label={t('propose')}
onChange={setProposal}
/>
<Static
label={t('preimage hash')}
value={encodedHash}
withCopy
/>
<Static
label={t('preimage length')}
value={encodedLength || '0'}
withCopy
/>
</Modal.Columns>
{!storageFee.isZero() && (
<Modal.Columns
className={className}
hint={t('The calculated storage costs based on the base and the per-bytes fee.')}
>
<InputBalance
defaultValue={storageFee}
isDisabled
label={t('calculated storage fee')}
/>
</Modal.Columns>
)}
</>
);
}
export default React.memo(Partial);
@@ -0,0 +1,74 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashState } from './types.js';
import React, { useState } from 'react';
import { Button, InputAddress, Modal, TxButton } from '@pezkuwi/react-components';
import { useToggle } from '@pezkuwi/react-hooks';
import { Available } from '@pezkuwi/react-query';
import { useTranslation } from '../../translate.js';
import Proposal, { EMPTY_PROPOSAL } from './Partial.js';
interface Props {
className?: string;
imageHash?: string;
}
function Add ({ className, imageHash }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [isAddOpen, toggleAdd] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [{ encodedHash, notePreimageTx }, setProposal] = useState<HashState>(EMPTY_PROPOSAL);
const isMatched = !imageHash || imageHash === encodedHash;
return (
<>
{isAddOpen && (
<Modal
className={className}
header={t('Submit preimage')}
onClose={toggleAdd}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t('This account will pay the fees for the preimage, based on the size thereof.')}>
<InputAddress
label={t('send from account')}
labelExtra={
<Available
label={<span className='label'>{t('transferable')}</span>}
params={accountId}
/>
}
onChange={setAccountId}
type='account'
/>
</Modal.Columns>
<Proposal onChange={setProposal} />
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={accountId}
extrinsic={notePreimageTx}
icon='plus'
isDisabled={!accountId || !isMatched || !notePreimageTx}
label={t('Submit preimage')}
onStart={toggleAdd}
/>
</Modal.Actions>
</Modal>
)}
<Button
icon='plus'
label={t('Add preimage')}
onClick={toggleAdd}
/>
</>
);
}
export default React.memo(Add);
@@ -0,0 +1,14 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { BN } from '@pezkuwi/util';
import type { HexString } from '@pezkuwi/util/types';
export interface HashState {
encodedHash: HexString;
encodedLength: number;
encodedProposal?: HexString | null;
notePreimageTx?: SubmittableExtrinsic<'promise'> | null;
storageFee: BN;
}
@@ -0,0 +1,96 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Preimage } from '@pezkuwi/react-hooks/types';
import React, { useMemo } from 'react';
import { AddressMini, CopyButton, MarkError, MarkWarning, styled } from '@pezkuwi/react-components';
import { ZERO_ACCOUNT } from '@pezkuwi/react-hooks/useWeight';
import { CallExpander } from '@pezkuwi/react-params';
import { Null } from '@pezkuwi/types-codec';
import { useTranslation } from '../translate.js';
interface Props {
className?: string;
noTmp?: boolean;
value?: Preimage;
}
function PreimageCall ({ className = '', value }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const link = useMemo(
() =>
value?.proposal
? `#/extrinsics/decode/${value?.proposal?.toHex()}`
: null,
[value]
);
return (
<>
<td className={`${className} all`}>
{value && value.isCompleted
? (
<>
{value.proposal && (
<CallExpander
labelHash={t('call')}
value={value.proposal}
/>
)}
{link && (
<StyledDiv>
<a
className='isDecoded'
href={link}
rel='noreferrer'
>{link.slice(0, 30)}...</a>
<CopyButton value={value.proposal?.toHex()} />
</StyledDiv>
)}
{value.proposalError
? <MarkError content={value.proposalError} />
: value.proposalWarning
? <MarkWarning content={value.proposalWarning} />
: null
}
</>
)
: <div className='--tmp'>balances.transfer</div>
}
</td>
<td className='address media--1300'>
{value && value.isCompleted
? value.deposit
? (
<AddressMini
// HACK: In the rare case that the value is passed down as a Null Codec type as seen with Tangle
// We ensure to handle that case. ref: https://github.com/pezkuwi-js/apps/issues/10793
balance={!(value.deposit.amount instanceof Null) ? value.deposit.amount : undefined}
value={value.deposit.who}
withBalance
/>
)
: null
: (
<AddressMini
className='--tmp'
value={ZERO_ACCOUNT}
/>
)}
</td>
</>
);
}
const StyledDiv = styled.div`
display: flex;
align-items: center;
margin: -0.4rem 0rem -0.4rem 1rem;
white-space: nowrap;
`;
export default React.memo(PreimageCall);
@@ -0,0 +1,43 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Preimage } from '@pezkuwi/react-hooks/types';
import React, { useMemo } from 'react';
import { TxButton } from '@pezkuwi/react-components';
import { useAccounts, useApi } from '@pezkuwi/react-hooks';
import { useTranslation } from '../translate.js';
interface Props {
className?: string;
value: Preimage;
}
function Free ({ className, value: { count, deposit, proposalHash, status } }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts } = useAccounts();
const isAvailable = useMemo(
() => count === 0 && status && status.isUnrequested && deposit && allAccounts.includes(deposit.who),
[allAccounts, count, deposit, status]
);
if (!isAvailable || !deposit) {
return null;
}
return (
<TxButton
accountId={deposit.who}
className={className}
icon='minus'
label={t('Unnote')}
params={[proposalHash]}
tx={api.tx.preimage.unnotePreimage}
/>
);
}
export default React.memo(Free);
@@ -0,0 +1,39 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@pezkuwi/util/types';
import React from 'react';
import { CopyButton, styled } from '@pezkuwi/react-components';
interface Props {
className?: string;
value: HexString;
}
function Hash ({ className = '', value }: Props): React.ReactElement<Props> {
return (
<StyledTd className={`${className} hash`}>
<div className='shortHash'>{value}</div>
<CopyButton value={value} />
</StyledTd>
);
}
const StyledTd = styled.td`
white-space: nowrap;
> div {
display: inline-block;
vertical-align: middle;
}
.shortHash {
+ div {
margin-left: 0.5rem;
}
}
`;
export default React.memo(Hash);
@@ -0,0 +1,52 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Preimage as TPreimage } from '@pezkuwi/react-hooks/types';
import type { HexString } from '@pezkuwi/util/types';
import React, { useEffect } from 'react';
import { usePreimage } from '@pezkuwi/react-hooks';
import { formatNumber } from '@pezkuwi/util';
import Call from './Call.js';
import Free from './Free.js';
import Hash from './Hash.js';
interface Props {
className?: string;
value: HexString;
cb?: (info: TPreimage) => void;
}
function Preimage ({ cb, className, value }: Props): React.ReactElement<Props> {
const info = usePreimage(value);
useEffect(() => {
info && cb?.(info);
}, [cb, info]);
return (
<tr className={className}>
<Hash value={value} />
<Call value={info} />
<td className='number media--1000'>
{info?.proposalLength
? formatNumber(info.proposalLength)
: <span className='--tmp'>999,999</span>}
</td>
<td className='preimageStatus together media--1200'>
{info
? (
<>
{info.status && (<div>{info.status?.type}{info.count !== 0 && <>&nbsp;/&nbsp;{formatNumber(info.count)}</>}</div>)}
<Free value={info} />
</>
)
: <span className='--tmp'>Unrequested</span>}
</td>
</tr>
);
}
export default React.memo(Preimage);
@@ -0,0 +1,31 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { CardSummary, SummaryBox } from '@pezkuwi/react-components';
import { formatNumber } from '@pezkuwi/util';
import { useTranslation } from '../translate.js';
interface Props {
className?: string;
hashes?: string[];
}
function Summary ({ className, hashes }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
return (
<SummaryBox className={className}>
<CardSummary label={t('images')}>
{hashes === undefined
? <span className='--tmp'>99</span>
: formatNumber(hashes.length)
}
</CardSummary>
</SummaryBox>
);
}
export default React.memo(Summary);
@@ -0,0 +1,90 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsicFunction } from '@pezkuwi/api/types';
import type { Preimage as TPreimage } from '@pezkuwi/react-hooks/types';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { Button, styled, Table } from '@pezkuwi/react-components';
import { useAccounts } from '@pezkuwi/react-hooks';
import { useTranslation } from '../translate.js';
import usePreimages from '../usePreimages.js';
import Add from './Add/index.js';
import UserPreimages from './userPreimages/index.js';
import Preimage from './Preimage.js';
import Summary from './Summary.js';
interface Props {
className?: string;
defaultPropose?: SubmittableExtrinsicFunction<'promise'>;
filter?: (section: string, method?: string) => boolean;
}
function Hashes ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { allAccounts } = useAccounts();
const [allPreImagesInfo, setAllPreImagesInfo] = useState<TPreimage[]>([]);
const hashes = usePreimages();
// HACK to concat all preimages info without creating a new hook, just for multiple hashes
const onSetAllPreImagesInfo = useCallback((info: TPreimage) => {
setAllPreImagesInfo((preimages) => ([
...preimages.filter((e) => e.proposalHash !== info.proposalHash),
info
]));
}, []);
const groupedUserPreimages = useMemo(() => {
return allPreImagesInfo.reduce((result: Record<string, TPreimage[]>, current) => {
if (current.deposit?.who && allAccounts.includes(current.deposit?.who)) {
const newItems = [...(result[current.deposit?.who] || []), current];
result[current.deposit?.who] = newItems;
}
return result;
}, {} as Record<string, TPreimage[]>);
}, [allAccounts, allPreImagesInfo]);
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('preimages'), 'start', 2],
[undefined, 'media--1300'],
[t('length'), 'media--1000'],
[t('status'), 'start media--1200']
]);
return (
<StyledDiv className={className}>
<Summary hashes={hashes} />
<Button.Group>
<Add />
</Button.Group>
<UserPreimages userPreimages={groupedUserPreimages} />
<Table
className={className}
empty={hashes && t('No hashes found')}
header={headerRef.current}
>
{hashes?.map((h) => (
<Preimage
cb={onSetAllPreImagesInfo}
key={h}
value={h}
/>
))}
</Table>
</StyledDiv>
);
}
const StyledDiv = styled.div`
td.preimageStatus {
div+.ui--Button {
margin-top: 0.25rem;
}
}
`;
export default React.memo(Hashes);
@@ -0,0 +1,151 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Preimage as TPreimage } from '@pezkuwi/react-hooks/types';
import React, { useState } from 'react';
import { AddressMini, Checkbox, styled, TxButton } from '@pezkuwi/react-components';
import { useApi } from '@pezkuwi/react-hooks';
import { formatNumber } from '@pezkuwi/util';
import { useTranslation } from '../../translate.js';
import Call from '../Call.js';
import Hash from '../Hash.js';
interface Props {
className?: string;
depositor: string,
preimageInfos: TPreimage[];
}
interface SelectPreimageProps {
proposalHash: TPreimage['proposalHash'],
onSelectPreimage: React.Dispatch<React.SetStateAction<TPreimage['proposalHash'][]>>
}
const SelectPreimage = ({ onSelectPreimage, proposalHash }: SelectPreimageProps) => {
const [checked, setChecked] = useState(false);
const onChange = React.useCallback((value: boolean) => {
setChecked(value);
onSelectPreimage((prevHashes) =>
// Add preimage hash if checked else filter it out
value ? [...prevHashes, proposalHash] : prevHashes.filter((i) => i !== proposalHash)
);
}, [onSelectPreimage, proposalHash]);
return (
<Checkbox
onChange={onChange}
value={checked}
/>
);
};
const Preimage = ({ className, depositor, preimageInfos }: Props) => {
const { t } = useTranslation();
const { api } = useApi();
const [selectedPreimages, onSelectPreimage] = useState<TPreimage['proposalHash'][]>([]);
return (
<>
{preimageInfos.map((info, index) => {
return (
<StyledTr
className={`isExpanded ${className}`}
isFirstItem={index === 0}
isLastItem={false}
key={info.proposalHash}
>
<td
className='address all'
style={{ paddingTop: 15, verticalAlign: 'top' }}
>
{index === 0 && <AddressMini value={depositor} />}
</td>
<Call value={info} />
<td style={{ alignItems: 'center', display: 'flex' }}>
<SelectPreimage
onSelectPreimage={onSelectPreimage}
proposalHash={info.proposalHash}
/>
<Hash value={info.proposalHash} />
</td>
<td className='number media--1000'>
{info?.proposalLength
? formatNumber(info.proposalLength)
: <span className='--tmp'>999,999</span>}
</td>
<td className='preimageStatus start media--1100 together'>
{info
? (
<>
{info.status && (<div>{info.status?.type}{info.count !== 0 && <>&nbsp;/&nbsp;{formatNumber(info.count)}</>}</div>)}
</>
)
: <span className='--tmp'>Unrequested</span>}
</td>
</StyledTr>
);
})}
<StyledTr
className={`isExpanded ${className}`}
isFirstItem={false}
isLastItem
>
<td className='all' />
<td className='all' />
<td className='media--1300' />
<td>
<TxButton
accountId={depositor}
className={className}
icon='minus'
isToplevel
label={t('Unnote')}
params={[selectedPreimages.map((i) => api.tx.preimage.unnotePreimage(i))]}
tx={api.tx.utility.batchAll}
/>
</td>
<td className='media--1000' />
<td className='media--1100' />
</StyledTr>
</>
);
};
const BASE_BORDER = 0.125;
const BORDER_TOP = `${BASE_BORDER * 3}rem solid var(--bg-page)`;
const BORDER_RADIUS = `${BASE_BORDER * 4}rem`;
const StyledTr = styled.tr<{isFirstItem: boolean; isLastItem: boolean}>`
background: var(--bg-table);
.ui--Icon {
border-width: 2px;
}
td {
border-top: ${(props) => props.isFirstItem && BORDER_TOP};
border-radius: 0rem !important;
&:first-child {
padding-block: 1rem !important;
border-top-left-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
border-bottom-left-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
}
&:last-child {
border-top-right-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
border-bottom-right-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
}
td {
border: none !important;
}
}
`;
export default React.memo(Preimage);
@@ -0,0 +1,46 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Preimage as TPreimage } from '@pezkuwi/react-hooks/types';
import React, { useRef } from 'react';
import { Table } from '@pezkuwi/react-components';
import { useTranslation } from '../../translate.js';
import Preimage from './Preimage.js';
interface Props {
className?: string;
userPreimages: Record<string, TPreimage[]>
}
const UserPreimages = ({ className, userPreimages }: Props) => {
const { t } = useTranslation();
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('my preimages'), 'start', 2],
[undefined, 'media--1300'],
[t('hash'), 'start'],
[t('length'), 'media--1000'],
[t('status'), 'start media--1100']
]);
return (
<Table
className={className}
empty={Object.values(userPreimages) && t('No hashes found')}
header={headerRef.current}
>
{Object.keys(userPreimages)?.map((depositor) => (
<Preimage
depositor={depositor}
key={depositor}
preimageInfos={userPreimages[depositor]}
/>
))}
</Table>
);
};
export default React.memo(UserPreimages);
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useRef } from 'react';
import { Tabs } from '@pezkuwi/react-components';
import Preimages from './Preimages/index.js';
import { useTranslation } from './translate.js';
interface Props {
basePath: string;
className?: string;
}
function App ({ basePath, className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const tabsRef = useRef([
{
isRoot: true,
name: 'overview',
text: t('Overview')
}
]);
return (
<main className={className}>
<Tabs
basePath={basePath}
items={tabsRef.current}
/>
<Preimages />
</main>
);
}
export default React.memo(App);
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2017-2025 @pezkuwi/app-preimages 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-preimages');
}
@@ -0,0 +1,50 @@
// Copyright 2017-2025 @pezkuwi/app-preimages authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Changes } from '@pezkuwi/react-hooks/useEventChanges';
import type { StorageKey } from '@pezkuwi/types';
import type { EventRecord, Hash } from '@pezkuwi/types/interfaces';
import type { HexString } from '@pezkuwi/util/types';
import { useMemo } from 'react';
import { createNamedHook, useApi, useEventChanges, useMapKeys } from '@pezkuwi/react-hooks';
const EMPTY_PARAMS: unknown[] = [];
const OPT_HASH = {
transform: (keys: StorageKey<[Hash]>[]): Hash[] =>
keys.map(({ args: [hash] }) => hash)
};
function filter (records: EventRecord[]): Changes<Hash> {
const added: Hash[] = [];
const removed: Hash[] = [];
records.forEach(({ event: { data: [hash], method } }): void => {
if (method === 'Noted') {
added.push(hash as Hash);
} else {
removed.push(hash as Hash);
}
});
return { added, removed };
}
function usePreimagesImpl (): HexString[] | undefined {
const { api } = useApi();
const startValueStatusFor = useMapKeys(api.query.preimage.statusFor, EMPTY_PARAMS, OPT_HASH);
const startvalueRequstStatusFor = useMapKeys(api.query.preimage.requestStatusFor, EMPTY_PARAMS, OPT_HASH);
const hashes = useEventChanges([
api.events.preimage.Cleared,
api.events.preimage.Noted
], filter, startValueStatusFor?.concat(startvalueRequstStatusFor || []));
return useMemo(
() => hashes?.map((h) => h.toHex()),
[hashes]
);
}
export default createNamedHook('usePreimages', usePreimagesImpl);