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:
Kurdistan Tech Ministry committed 2026-01-07 13:05:27 +03:00
commit d21bfb1320
5867 files changed
+329019

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { AccountId, Balance, BlockNumber, OpenTipTo225 } from '@pezkuwi/types/interfaces';
import type { PalletTipsOpenTip } from '@pezkuwi/types/lookup';
import type { BN } from '@pezkuwi/util';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { AddressMini, AddressSmall, Checkbox, ExpanderScroll, Icon, LinkExternal, styled, TxButton } from '@pezkuwi/react-components';
import { useAccounts, useApi } from '@pezkuwi/react-hooks';
import { BlockToTime, FormatBalance } from '@pezkuwi/react-query';
import { BN_ZERO, formatNumber } from '@pezkuwi/util';
import { useTranslation } from '../translate.js';
import TipEndorse from './TipEndorse.js';
import TipReason from './TipReason.js';
interface Props {
bestNumber?: BlockNumber;
className?: string;
defaultId: string | null;
hash: string;
isMember: boolean;
members: string[];
onSelect: (hash: string, isSelected: boolean, value: BN) => void;
onlyUntipped: boolean;
tip: PalletTipsOpenTip | OpenTipTo225;
}
interface TipState {
closesAt: BlockNumber | null;
deposit: Balance | null;
finder: AccountId | null;
isFinder: boolean;
isTipped: boolean;
isTipper: boolean;
median: BN;
}
function isCurrentTip (tip: PalletTipsOpenTip | OpenTipTo225): tip is PalletTipsOpenTip {
return !!(tip as PalletTipsOpenTip)?.findersFee;
}
function extractTipState (tip: PalletTipsOpenTip | OpenTipTo225, allAccounts: string[]): TipState {
const closesAt = tip.closes.unwrapOr(null);
let finder: AccountId | null = null;
let deposit: Balance | null = null;
if (isCurrentTip(tip)) {
finder = tip.finder;
deposit = tip.deposit;
} else if (tip.finder.isSome) {
const finderInfo = tip.finder.unwrap();
finder = finderInfo[0];
deposit = finderInfo[1];
}
const values = tip.tips.map(([, value]) => value).sort((a, b) => a.cmp(b));
const midIndex = Math.floor(values.length / 2);
const median = values.length
? values.length % 2
? values[midIndex]
: values[midIndex - 1].add(values[midIndex]).divn(2)
: BN_ZERO;
return {
closesAt,
deposit,
finder,
isFinder: !!finder && allAccounts.includes(finder.toString()),
isTipped: !!values.length,
isTipper: tip.tips.some(([address]) => allAccounts.includes(address.toString())),
median
};
}
function Tip ({ bestNumber, className = '', defaultId, hash, isMember, members, onSelect, onlyUntipped, tip }: Props): React.ReactElement<Props> | null {
const { api } = useApi();
const { t } = useTranslation();
const { allAccounts } = useAccounts();
const { closesAt, finder, isFinder, isTipped, isTipper, median } = useMemo(
() => extractTipState(tip, allAccounts),
[allAccounts, tip]
);
const councilId = useMemo(
() => allAccounts.find((accountId) => members.includes(accountId)) || null,
[allAccounts, members]
);
const [isMedianSelected, setMedianTip] = useState(false);
const renderTippers = useCallback(
() => tip.tips.map(([tipper, balance]) => (
<AddressMini
balance={balance}
key={tipper.toString()}
value={tipper}
withBalance
/>
)),
[tip]
);
useEffect((): void => {
onSelect(hash, isMedianSelected, median);
}, [hash, isMedianSelected, median, onSelect]);
useEffect((): void => {
setMedianTip(isMember && !isTipper);
}, [isMember, isTipper]);
if (onlyUntipped && !closesAt && isTipper) {
return null;
}
const { reason, tips, who } = tip;
const recipient = who.toString();
return (
<StyledTr className={className}>
<td className='address'>
<AddressSmall value={who} />
</td>
<td className='address media--1400'>
{finder && (
<AddressMini value={finder} />
)}
</td>
<TipReason hash={reason} />
<td className='expand media--1100'>
{tips.length !== 0 && (
<ExpanderScroll
renderChildren={renderTippers}
summary={
<>
<div>{t('Tippers ({{count}})', { replace: { count: tips.length } })}</div>
<FormatBalance value={median} />
</>
}
/>
)}
</td>
<td className='button together'>
{closesAt
? (bestNumber && closesAt.gt(bestNumber)) && (
<div className='closingTimer'>
<BlockToTime value={closesAt.sub(bestNumber)} />
#{formatNumber(closesAt)}
</div>
)
: finder && isFinder && (
<TxButton
accountId={finder}
className='media--1400'
icon='times'
label={t('Cancel')}
params={[hash]}
tx={(api.tx.tips || api.tx.treasury).retractTip}
/>
)
}
{(!closesAt || !bestNumber || closesAt.gt(bestNumber))
? (
<TipEndorse
defaultId={defaultId}
hash={hash}
isMember={isMember}
isTipped={isTipped}
median={median}
members={members}
recipient={recipient}
/>
)
: (
<TxButton
accountId={councilId}
icon='times'
label={t('Close')}
params={[hash]}
tx={(api.tx.tips || api.tx.treasury).closeTip}
/>
)
}
</td>
<td className='badge media--1700'>
{isMember && (
<Icon
color={isTipper ? 'green' : 'gray'}
icon='asterisk'
/>
)}
</td>
<td>
<Checkbox
isDisabled={!isMember}
onChange={setMedianTip}
value={isMedianSelected}
/>
</td>
<td className='links media--1700'>
<LinkExternal
data={hash}
type='tip'
/>
</td>
</StyledTr>
);
}
const StyledTr = styled.tr`
.closingTimer {
display: inline-block;
padding: 0 0.5rem;
}
`;
export default React.memo(Tip);
@@ -0,0 +1,119 @@
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@pezkuwi/util';
import React, { useMemo, useState } from 'react';
import { Button, Input, InputAddress, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
import { useApi, useToggle } from '@pezkuwi/react-hooks';
import { BN_ZERO } from '@pezkuwi/util';
import { useTranslation } from '../translate.js';
interface Props {
members: string[];
}
const MAX_REASON_LEN = 256;
const MIN_REASON_LEN = 5;
function TipCreate ({ members }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { api } = useApi();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(null);
const [beneficiary, setBeneficiary] = useState<string | null>(null);
const [reason, setReason] = useState('');
const [value, setValue] = useState<BN | undefined>();
const maxReasonLen = useMemo(
() => Math.min(MAX_REASON_LEN, (
(api.consts.tips || api.consts.treasury)?.maximumReasonLength?.toNumber() ||
MAX_REASON_LEN
)),
[api]
);
const isMember = useMemo(
() => !!accountId && members.includes(accountId),
[accountId, members]
);
const hasValue = !!value && value.gt(BN_ZERO);
const hasReason = !!reason && (reason.length >= MIN_REASON_LEN) && (reason.length <= maxReasonLen);
if (!(api.tx.tips.tipNew || api.tx.treasury.tipNew)) {
return null;
}
return (
<>
<Button
icon='plus'
label={t('Propose tip')}
onClick={toggleOpen}
/>
{isOpen && (
<Modal
header={t('Submit tip request')}
onClose={toggleOpen}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t('Use this account to request the tip from. This can be a normal or council account.')}>
<InputAddress
label={t('submit with account')}
onChange={setAccountId}
type='account'
withLabel
/>
</Modal.Columns>
<Modal.Columns hint={t('The beneficiary will received the tip as approved by council members.')}>
<InputAddress
label={t('beneficiary')}
onChange={setBeneficiary}
type='allPlus'
/>
</Modal.Columns>
<Modal.Columns hint={t('A reason (to be stored-on-chain) as to why the recipient deserves a tip payout.')}>
<Input
autoFocus
isError={!hasReason}
label={t('tip reason')}
onChange={setReason}
/>
</Modal.Columns>
{isMember && (
<Modal.Columns hint={t('As a council member, you can suggest an initial value for the tip, each other council member can suggest their own.')}>
<InputBalance
isError={!hasValue}
label={t('tip value')}
onChange={setValue}
/>
</Modal.Columns>
)}
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={accountId}
icon='plus'
isDisabled={!accountId || (isMember && !hasValue) || !hasReason}
label={t('Propose tip')}
onStart={toggleOpen}
params={
isMember
? [reason, beneficiary, value]
: [reason, beneficiary]
}
tx={
isMember
? (api.tx.tips || api.tx.treasury).tipNew
: (api.tx.tips || api.tx.treasury).reportAwesome
}
/>
</Modal.Actions>
</Modal>
)}
</>
);
}
export default React.memo(TipCreate);
@@ -0,0 +1,103 @@
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveBalancesAll } from '@pezkuwi/api-derive/types';
import type { BN } from '@pezkuwi/util';
import React, { useState } from 'react';
import { Button, InputAddress, InputBalance, MarkWarning, Modal, TxButton } from '@pezkuwi/react-components';
import { useApi, useCall, useToggle } from '@pezkuwi/react-hooks';
import { useTranslation } from '../translate.js';
interface Props {
defaultId: string | null;
hash: string;
isMember: boolean;
isTipped: boolean;
median: BN;
members: string[];
recipient: string;
}
const OPT = {
transform: ({ freeBalance, reservedBalance }: DeriveBalancesAll): BN =>
freeBalance.add(reservedBalance)
};
function TipEndorse ({ defaultId, hash, isMember, isTipped, median, members, recipient }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [isOpen, toggleOpen] = useToggle();
const [accountId, setAccountId] = useState<string | null>(defaultId);
const [value, setValue] = useState<BN | undefined>();
const totalBalance = useCall<BN>(api.derive.balances?.all, [recipient], OPT);
const tipTx = (api.tx.tips || api.tx.treasury).tip;
return (
<>
<Button
icon='check'
isDisabled={!isMember}
label={t('Tip')}
onClick={toggleOpen}
/>
<TxButton
accountId={defaultId}
className='media--1600'
icon='fighter-jet'
isDisabled={!isMember || !isTipped}
isIcon
params={[hash, median]}
tx={tipTx}
withoutLink
/>
{isOpen && (
<Modal
header={t('Submit tip endorsement')}
onClose={toggleOpen}
size='large'
>
<Modal.Content>
<Modal.Columns hint={t('Your endorsement will be applied for this account.')}>
<InputAddress
filter={members}
label={t('submit with account')}
onChange={setAccountId}
type='account'
withLabel
/>
</Modal.Columns>
<Modal.Columns hint={t('Allocate a suggested tip amount. With enough endorsements, the suggested values are averaged and sent to the beneficiary.')}>
<InputBalance
autoFocus
defaultValue={median}
isZeroable
label={t('value')}
onChange={setValue}
/>
{totalBalance && totalBalance.isZero() && (
<MarkWarning content={t('The recipient account has no balance, ensure the tip is more than the existential deposit to create the account.')} />
)}
</Modal.Columns>
</Modal.Content>
<Modal.Actions>
<TxButton
accountId={accountId}
icon='plus'
isDisabled={!accountId}
label={t('Submit tip')}
onStart={toggleOpen}
params={[hash, value]}
tx={tipTx}
/>
</Modal.Actions>
</Modal>
)}
</>
);
}
export default React.memo(TipEndorse);
@@ -0,0 +1,32 @@
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Bytes, Option } from '@pezkuwi/types';
import type { Hash } from '@pezkuwi/types/interfaces';
import React from 'react';
import { useApi, useCall } from '@pezkuwi/react-hooks';
import { hexToString } from '@pezkuwi/util';
interface Props {
hash: Hash;
}
const OPT = {
transform: (optBytes: Option<Bytes>) =>
optBytes.isSome
? hexToString(optBytes.unwrap().toHex())
: null
};
function TipReason ({ hash }: Props): React.ReactElement<Props> {
const { api } = useApi();
const reasonText = useCall<string | null>((api.query.tips || api.query.treasury).reasons, [hash], OPT);
return (
<td className='start all'>{reasonText || hash.toHex()}</td>
);
}
export default React.memo(TipReason);
+117
View File
@@ -0,0 +1,117 @@
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Option } from '@pezkuwi/types';
import type { OpenTipTo225 } from '@pezkuwi/types/interfaces';
import type { PalletTipsOpenTip } from '@pezkuwi/types/lookup';
import type { BN } from '@pezkuwi/util';
import React, { useMemo, useRef, useState } from 'react';
import { styled, Table, Toggle } from '@pezkuwi/react-components';
import { useApi, useBestNumber, useCall } from '@pezkuwi/react-hooks';
import { useTranslation } from '../translate.js';
import Tip from './Tip.js';
interface Props {
className?: string;
defaultId: string | null;
hashes?: string[] | null;
isMember: boolean;
members: string[];
onSelectTip: (hash: string, isSelected: boolean, value: BN) => void,
}
type TipType = [string, PalletTipsOpenTip | OpenTipTo225];
const TIP_OPTS = { withParams: true };
function extractTips (tipsWithHashes?: [[string[]], Option<PalletTipsOpenTip>[]], inHashes?: string[] | null): TipType[] | undefined {
if (!tipsWithHashes || !inHashes) {
return undefined;
}
const [[hashes], optTips] = tipsWithHashes;
return optTips
.map((opt, index): [string, PalletTipsOpenTip | null] => [hashes[index], opt.unwrapOr(null)])
.filter((val): val is [string, PalletTipsOpenTip] => inHashes.includes(val[0]) && !!val[1])
.sort((a, b) =>
a[1].closes.isNone
? b[1].closes.isNone
? 0
: -1
: b[1].closes.isSome
? b[1].closes.unwrap().cmp(a[1].closes.unwrap())
: 1
);
}
function Tips ({ className = '', defaultId, hashes, isMember, members, onSelectTip }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const [onlyUntipped, setOnlyUntipped] = useState(false);
const bestNumber = useBestNumber();
const tipsWithHashes = useCall<[[string[]], Option<PalletTipsOpenTip>[]]>(hashes && (api.query.tips || api.query.treasury).tips.multi, [hashes], TIP_OPTS);
const tips = useMemo(
() => extractTips(tipsWithHashes, hashes),
[hashes, tipsWithHashes]
);
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
[t('tips'), 'start'],
[t('finder'), 'address media--1400'],
[t('reason'), 'start'],
[undefined, 'media--1100'],
[],
[undefined, 'badge media--1700'],
[],
[undefined, 'media--1700']
]);
return (
<StyledTable
className={className}
empty={tips && t('No open tips')}
filter={isMember && (
<div className='tipsFilter'>
<Toggle
label={t('show only untipped/closing')}
onChange={setOnlyUntipped}
value={onlyUntipped}
/>
</div>
)}
header={headerRef.current}
>
{tips?.map(([hash, tip]): React.ReactNode => (
<Tip
bestNumber={bestNumber}
defaultId={defaultId}
hash={hash}
isMember={isMember}
key={hash}
members={members}
onSelect={onSelectTip}
onlyUntipped={onlyUntipped}
tip={tip}
/>
))}
</StyledTable>
);
}
const StyledTable = styled(Table)`
.tipsFilter {
text-align: right;
.ui--Toggle {
margin-right: 1rem;
margin-top: 0.75rem;
}
}
`;
export default React.memo(Tips);
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2017-2025 @pezkuwi/app-treasury authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubmittableExtrinsic } from '@pezkuwi/api/types';
import type { BN } from '@pezkuwi/util';
import React, { useCallback, useMemo, useState } from 'react';
import { Button, TxButton } from '@pezkuwi/react-components';
import { useAccounts, useApi, useTxBatch } from '@pezkuwi/react-hooks';
import { useTranslation } from '../translate.js';
import TipCreate from './TipCreate.js';
import Tips from './Tips.js';
interface Props {
className?: string;
hashes?: string[] | null;
isMember: boolean;
members: string[];
}
interface QuickTipsState {
quickTips: Record<string, BN | null>;
quickTxs: SubmittableExtrinsic<'promise'>[];
}
const DEFAULT_TIPS = { quickTips: {}, quickTxs: [] };
function TipsEntry ({ className, hashes, isMember, members }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { allAccounts } = useAccounts();
const { api } = useApi();
const [{ quickTxs }, setQuickTips] = useState<QuickTipsState>(DEFAULT_TIPS);
const batchTxs = useTxBatch(quickTxs);
const defaultId = useMemo(
() => members.find((memberId) => allAccounts.includes(memberId)) || null,
[allAccounts, members]
);
const _selectTip = useCallback(
(hash: string, isSelected: boolean, value: BN) => setQuickTips(({ quickTips }): QuickTipsState => {
quickTips[hash] = isSelected ? value : null;
return {
quickTips,
quickTxs: Object
.entries(quickTips)
.map(([hash, value]) => value && (api.tx.tips || api.tx.treasury).tip(hash, value))
.filter((value): value is SubmittableExtrinsic<'promise'> => !!value)
};
}),
[api]
);
return (
<div className={className}>
<Button.Group>
<TipCreate members={members} />
<TxButton
accountId={defaultId}
extrinsic={batchTxs}
icon='fighter-jet'
isDisabled={!isMember || !batchTxs}
label={t('Median tip selected')}
/>
</Button.Group>
<Tips
defaultId={defaultId}
hashes={hashes}
isMember={isMember}
members={members}
onSelectTip={_selectTip}
/>
</div>
);
}
export default React.memo(TipsEntry);