mirror of
https://github.com/pezkuwichain/pezkuwi-apps.git
synced 2026-06-12 20:31:14 +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,69 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// augment package
|
||||
import '@pezkuwi/api-augment/bizinikiwi';
|
||||
|
||||
import type { PalletAssetsAssetAccount } from '@pezkuwi/types/lookup';
|
||||
import type { bool } from '@pezkuwi/types-codec';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressSmall } from '@pezkuwi/react-components';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Transfer from './Transfer.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore This looks correct in the editor, but incorrect in composite mode
|
||||
interface AccountExt extends PalletAssetsAssetAccount {
|
||||
isFrozen?: bool;
|
||||
sufficient?: bool
|
||||
}
|
||||
|
||||
interface Props {
|
||||
account: AccountExt;
|
||||
accountId: string;
|
||||
assetId: BN;
|
||||
className?: string;
|
||||
minBalance: BN;
|
||||
siFormat: [number, string];
|
||||
}
|
||||
|
||||
function Account ({ account: { balance, isFrozen, reason, sufficient }, accountId, assetId, minBalance, siFormat }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<td className='address'>
|
||||
<AddressSmall value={accountId} />
|
||||
</td>
|
||||
<td className='start'>
|
||||
{isFrozen?.isTrue ? t('Yes') : t('No')}
|
||||
</td>
|
||||
<td className='start'>
|
||||
{sufficient
|
||||
? sufficient.isTrue ? t('Yes') : t('No')
|
||||
: reason?.toString()}
|
||||
</td>
|
||||
<td className='number all'>
|
||||
<FormatBalance
|
||||
format={siFormat}
|
||||
value={balance}
|
||||
/>
|
||||
</td>
|
||||
<td className='button'>
|
||||
<Transfer
|
||||
accountId={accountId}
|
||||
assetId={assetId}
|
||||
minBalance={minBalance}
|
||||
siFormat={siFormat}
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Account);
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfoComplete } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { styled } from '@pezkuwi/react-components';
|
||||
import { formatNumber } from '@pezkuwi/util';
|
||||
|
||||
import Account from './Account.js';
|
||||
import useBalances from './useBalances.js';
|
||||
|
||||
interface Props {
|
||||
asset: AssetInfoComplete,
|
||||
className?: string;
|
||||
searchValue: string;
|
||||
}
|
||||
|
||||
const Asset = ({ asset: { details, id, metadata }, className, searchValue }: Props) => {
|
||||
const balances = useBalances(id);
|
||||
|
||||
const siFormat = useMemo(
|
||||
(): [number, string] => metadata
|
||||
? [metadata.decimals.toNumber(), metadata.symbol.toUtf8().toUpperCase()]
|
||||
: [0, 'NONE'],
|
||||
[metadata]
|
||||
);
|
||||
|
||||
const shouldShowAsset = useMemo(() => metadata.name.toUtf8().toLowerCase().includes(searchValue) ||
|
||||
formatNumber(id).toString().replaceAll(',', '').includes(searchValue), [id, metadata.name, searchValue]);
|
||||
|
||||
if (!balances?.length || !shouldShowAsset) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return balances.map(({ account, accountId }, index) => {
|
||||
return (
|
||||
<StyledTr
|
||||
className={`isExpanded ${className}`}
|
||||
isFirstItem={index === 0}
|
||||
isLastItem={index === balances.length - 1}
|
||||
key={accountId}
|
||||
>
|
||||
<td className='all'>
|
||||
{index === 0 && <>{metadata.name.toUtf8()} ({formatNumber(id)})</>}
|
||||
</td>
|
||||
<Account
|
||||
account={account}
|
||||
accountId={accountId}
|
||||
assetId={id}
|
||||
key={accountId}
|
||||
minBalance={details.minBalance}
|
||||
siFormat={siFormat}
|
||||
/>
|
||||
</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}>`
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Asset;
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, InputAddress, InputBalance, Modal, Toggle, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
accountId: string;
|
||||
assetId: BN;
|
||||
className?: string;
|
||||
minBalance: BN;
|
||||
siFormat: [number, string];
|
||||
}
|
||||
|
||||
function Transfer ({ accountId, assetId, className, minBalance, siFormat: [siDecimals, siSymbol] }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
const [amount, setAmount] = useState<BN | undefined>();
|
||||
const [recipientId, setRecipientId] = useState<string | null>(null);
|
||||
const [isProtected, setIsProtected] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='paper-plane'
|
||||
label={t('send')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('transfer asset')}
|
||||
onClose={toggleOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The account to transfer from. This account should have sufficient assets for this transfer.')}>
|
||||
<InputAddress
|
||||
defaultValue={accountId}
|
||||
isDisabled
|
||||
label={t('send from')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The beneficiary will have access to the transferred asset when the transaction is included in a block.')}>
|
||||
<InputAddress
|
||||
label={t('send to address')}
|
||||
onChange={setRecipientId}
|
||||
type='allPlus'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The amount of tokens to transfer to the account.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
label={t('amount to transfer')}
|
||||
onChange={setAmount}
|
||||
siDecimals={siDecimals}
|
||||
siSymbol={siSymbol}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The minimum balance allowed for the asset.')}>
|
||||
<InputBalance
|
||||
defaultValue={minBalance}
|
||||
isDisabled
|
||||
label={t('minimum balance')}
|
||||
siDecimals={siDecimals}
|
||||
siSymbol={siSymbol}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('With the keep-alive option set, the account is protected against removal due to low balances.')}>
|
||||
<Toggle
|
||||
className='typeToggle'
|
||||
label={
|
||||
isProtected
|
||||
? t('Transfer with account keep-alive checks')
|
||||
: t('Normal transfer without keep-alive checks')
|
||||
}
|
||||
onChange={setIsProtected}
|
||||
value={isProtected}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={accountId}
|
||||
icon='paper-plane'
|
||||
isDisabled={!recipientId || !amount}
|
||||
label={t('Send')}
|
||||
onStart={toggleOpen}
|
||||
params={[assetId, recipientId, amount]}
|
||||
tx={
|
||||
isProtected
|
||||
? api.tx.assets.transferKeepAlive
|
||||
: api.tx.assets.transfer}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Transfer);
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfo, AssetInfoComplete } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Input, styled, Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Asset from './Asset.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
infos?: AssetInfo[];
|
||||
}
|
||||
|
||||
function Balances ({ className, infos = [] }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [{ searchValue }, onApplySearch] = useState({ searchValue: '' });
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('asset'), 'start'],
|
||||
[t('accounts'), 'start'],
|
||||
[t('frozen'), 'start'],
|
||||
[t('sufficient'), 'start'],
|
||||
[t('free balance'), 'start'],
|
||||
[]
|
||||
]);
|
||||
|
||||
const onChangeInput = useCallback((e: string) => {
|
||||
onApplySearch({ searchValue: e });
|
||||
}, []);
|
||||
|
||||
const completeAssets = useMemo(
|
||||
() => infos
|
||||
.filter((i): i is AssetInfoComplete => !!(i.details && i.metadata) && !i.details.supply.isZero())
|
||||
,
|
||||
[infos]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<Input
|
||||
aria-label={t('Search by asset id or name')}
|
||||
className='full isSmall'
|
||||
label={t('Search')}
|
||||
onChange={onChangeInput}
|
||||
placeholder={t('Search by asset id or name')}
|
||||
value={searchValue}
|
||||
/>
|
||||
<Table
|
||||
empty={t('No accounts with balances found for the asset')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{completeAssets.map((asset) => {
|
||||
return (
|
||||
<Asset
|
||||
asset={asset}
|
||||
key={asset.id.toString()}
|
||||
searchValue={searchValue.toLowerCase()}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
input {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
table {
|
||||
overflow: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Balances);
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletAssetsAssetAccount } from '@pezkuwi/types/lookup';
|
||||
import type { Option } from '@pezkuwi/types-codec';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
interface AccountResult {
|
||||
accountId: string;
|
||||
account: PalletAssetsAssetAccount;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
assetId: BN;
|
||||
accounts: AccountResult[];
|
||||
}
|
||||
|
||||
function isOptional (value: PalletAssetsAssetAccount | Option<PalletAssetsAssetAccount>): value is Option<PalletAssetsAssetAccount> {
|
||||
return (value as Option<PalletAssetsAssetAccount>).isSome || (value as Option<PalletAssetsAssetAccount>).isNone;
|
||||
}
|
||||
|
||||
const OPTS = {
|
||||
transform: ([[params], accounts]: [[[BN, string][]], (PalletAssetsAssetAccount | Option<PalletAssetsAssetAccount>)[]]): Result => ({
|
||||
accounts: params
|
||||
.map(([, accountId], index) => {
|
||||
const o = accounts[index];
|
||||
|
||||
return {
|
||||
account: isOptional(o)
|
||||
? o.unwrapOr(null)
|
||||
: o,
|
||||
accountId
|
||||
};
|
||||
})
|
||||
.filter((a): a is AccountResult =>
|
||||
!!a.account &&
|
||||
!a.account.balance.isZero()
|
||||
),
|
||||
assetId: params[0][0]
|
||||
}),
|
||||
withParamsTransform: true
|
||||
};
|
||||
|
||||
function useBalancesImpl (id?: BN | null): AccountResult[] | null {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
const keys = useMemo(
|
||||
() => [allAccounts.map((a) => [id, a]).filter((tup) => !!tup[0])],
|
||||
[allAccounts, id]
|
||||
);
|
||||
const query = useCall(keys && api.query.assets.account.multi, keys, OPTS);
|
||||
|
||||
return (query && id && (query.assetId === id) && query.accounts) || null;
|
||||
}
|
||||
|
||||
export default createNamedHook('useBalances', useBalancesImpl);
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfo } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall } from '@pezkuwi/react-components';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
|
||||
import Mint from './Mint/index.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: AssetInfo;
|
||||
}
|
||||
|
||||
function Asset ({ className, value: { details, id, isIssuerMe, metadata } }: Props): React.ReactElement<Props> {
|
||||
const format = useMemo(
|
||||
(): [number, string] => metadata
|
||||
? [metadata.decimals.toNumber(), metadata.symbol.toUtf8()]
|
||||
: [0, '---'],
|
||||
[metadata]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<td></td>
|
||||
<td className='together'>
|
||||
<span style={{ fontSize: 18, marginRight: 10 }}>
|
||||
{id.toString()}
|
||||
</span>
|
||||
{metadata?.name.toUtf8()}</td>
|
||||
<td className='address media--1000'>{details && <AddressSmall value={details.owner} />}</td>
|
||||
<td className='address media--1300'>{details && <AddressSmall value={details.admin} />}</td>
|
||||
<td className='address media--1600'>{details && <AddressSmall value={details.issuer} />}</td>
|
||||
<td className='address media--1900'>{details && <AddressSmall value={details.freezer} />}</td>
|
||||
<td className='number all'>{details && (
|
||||
<FormatBalance
|
||||
format={format}
|
||||
value={details.supply}
|
||||
/>
|
||||
)}</td>
|
||||
<td className='button'>{details && metadata && isIssuerMe && (
|
||||
<Mint
|
||||
details={details}
|
||||
id={id}
|
||||
metadata={metadata}
|
||||
/>
|
||||
)}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Asset);
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfo } from '@pezkuwi/react-hooks/types';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Asset from './Asset.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
infos?: AssetInfo[];
|
||||
}
|
||||
|
||||
function Assets ({ className, infos }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('assets'), 'start', 2],
|
||||
[t('owner'), 'address media--1000'],
|
||||
[t('admin'), 'address media--1300'],
|
||||
[t('issuer'), 'address media--1600'],
|
||||
[t('freezer'), 'address media--1900'],
|
||||
[t('supply')],
|
||||
[]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={infos && t('No assets found')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{infos?.map((info) => (
|
||||
<Asset
|
||||
key={info.key}
|
||||
value={info}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Assets);
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BatchOptions } from '@pezkuwi/react-hooks/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { InfoState, TeamState } from './types.js';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { Button, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi, useStepper, useTxBatch } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import Info from './Info.js';
|
||||
import Team from './Team.js';
|
||||
|
||||
interface Props {
|
||||
assetIds: BN[];
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
openId: BN;
|
||||
}
|
||||
|
||||
const BATCH_OPTS: BatchOptions = { type: 'all' };
|
||||
|
||||
function Create ({ assetIds, className, onClose, openId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [step, nextStep, prevStep] = useStepper();
|
||||
const [asset, setAsset] = useState<InfoState | null>(null);
|
||||
const [team, setTeam] = useState<TeamState | null>(null);
|
||||
|
||||
const [createTx, metadataTx] = useMemo(
|
||||
() => asset
|
||||
? [
|
||||
api.tx.assets.create(asset.assetId, asset.accountId, asset.minBalance),
|
||||
api.tx.assets.setMetadata(asset.assetId, asset.assetName, asset.assetSymbol, asset.assetDecimals)
|
||||
]
|
||||
: [null, null],
|
||||
[api, asset]
|
||||
);
|
||||
|
||||
const teamTx = useMemo(
|
||||
() => asset && team && (team.adminId !== asset.accountId || team.freezerId !== asset.accountId || team.issuerId !== asset.accountId)
|
||||
? api.tx.assets.setTeam(asset.assetId, team.issuerId, team.adminId, team.freezerId)
|
||||
: null,
|
||||
[api, asset, team]
|
||||
);
|
||||
|
||||
const txs = useMemo(
|
||||
() => createTx && metadataTx && team && (
|
||||
teamTx
|
||||
? [createTx, metadataTx, teamTx]
|
||||
: [createTx, metadataTx]
|
||||
),
|
||||
[createTx, metadataTx, team, teamTx]
|
||||
);
|
||||
|
||||
const extrinsic = useTxBatch(txs, BATCH_OPTS);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('create asset {{step}}/{{steps}}', { replace: { step, steps: 2 } })}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
{step === 1 && (
|
||||
<Info
|
||||
assetIds={assetIds}
|
||||
defaultValue={asset}
|
||||
onChange={setAsset}
|
||||
openId={openId}
|
||||
/>
|
||||
)}
|
||||
{step === 2 && asset && (
|
||||
<Team
|
||||
accountId={asset.accountId}
|
||||
defaultValue={team}
|
||||
onChange={setTeam}
|
||||
/>
|
||||
)}
|
||||
<Modal.Actions>
|
||||
{step === 1 &&
|
||||
<Button
|
||||
icon='step-forward'
|
||||
isDisabled={!asset}
|
||||
label={t('Next')}
|
||||
onClick={nextStep}
|
||||
/>
|
||||
}
|
||||
{step === 2 && (
|
||||
<>
|
||||
<Button
|
||||
icon='step-backward'
|
||||
label={t('Prev')}
|
||||
onClick={prevStep}
|
||||
/>
|
||||
<TxButton
|
||||
accountId={asset?.accountId}
|
||||
extrinsic={extrinsic}
|
||||
icon='plus'
|
||||
label={t('Create')}
|
||||
onStart={onClose}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Create);
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BitLength } from '@pezkuwi/react-components/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { InfoState } from './types.js';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Input, InputAddress, InputBalance, InputNumber, Modal } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
import { BN_ZERO } from '@pezkuwi/util';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
|
||||
interface Props {
|
||||
assetIds: BN[];
|
||||
className?: string;
|
||||
defaultValue: InfoState | null;
|
||||
onChange: (info: InfoState | null) => void;
|
||||
openId: BN;
|
||||
}
|
||||
|
||||
const ASSET_ID_BIT_LENGTH: BitLength = 128;
|
||||
|
||||
function Info ({ assetIds, className = '', defaultValue, onChange, openId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [initial] = useState(() => defaultValue);
|
||||
const [initialId] = useState(() => openId);
|
||||
const [accountId, setAccountId] = useState<string | null>(null);
|
||||
const [assetId, setAssetId] = useState<BN | undefined>();
|
||||
const [assetDecimals, setAssetDecimals] = useState<BN | undefined>();
|
||||
const [assetName, setAssetName] = useState<string | null | undefined>(() => defaultValue?.assetName);
|
||||
const [assetSymbol, setAssetSymbol] = useState<string | null | undefined>(() => defaultValue?.assetSymbol);
|
||||
const [minBalance, setMinBalance] = useState<BN | undefined>();
|
||||
|
||||
const [siDecimals, siSymbol] = useMemo(
|
||||
() => assetDecimals && assetSymbol
|
||||
? [assetDecimals.toNumber(), assetSymbol.toUpperCase()]
|
||||
: [0, 'NONE'],
|
||||
[assetDecimals, assetSymbol]
|
||||
);
|
||||
|
||||
const isValidDecimals = useMemo(
|
||||
() => !!assetDecimals && assetDecimals.lten(20),
|
||||
[assetDecimals]
|
||||
);
|
||||
|
||||
const isValidName = useMemo(
|
||||
() => !!assetName && assetName.length >= 3 && assetName.length <= 32,
|
||||
[assetName]
|
||||
);
|
||||
|
||||
const isValidSymbol = useMemo(
|
||||
() => !!assetSymbol && assetSymbol.length >= 3 && assetSymbol.length <= 7,
|
||||
[assetSymbol]
|
||||
);
|
||||
|
||||
const isValidId = useMemo(
|
||||
() => !!assetId && assetId.gt(BN_ZERO) && !assetIds.some((a) => a.eq(assetId)),
|
||||
[assetId, assetIds]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange(
|
||||
assetId && assetName && assetSymbol && assetDecimals && isValidId && isValidName && isValidSymbol && isValidDecimals && accountId && minBalance && !minBalance.isZero()
|
||||
? { accountId, assetDecimals, assetId, assetName, assetSymbol, minBalance }
|
||||
: null
|
||||
);
|
||||
}, [api, accountId, assetDecimals, assetId, assetIds, assetName, assetSymbol, isValidId, isValidName, isValidSymbol, isValidDecimals, minBalance, onChange]);
|
||||
|
||||
return (
|
||||
<Modal.Content className={className}>
|
||||
<Modal.Columns hint={t('The account that is to be used to create this asset and setup the initial metadata.')}>
|
||||
<InputAddress
|
||||
defaultValue={initial?.accountId}
|
||||
label={t('creator account')}
|
||||
onChange={setAccountId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The descriptive name for this asset.')}>
|
||||
<Input
|
||||
autoFocus
|
||||
defaultValue={initial?.assetName}
|
||||
isError={!isValidName}
|
||||
label={t('asset name')}
|
||||
onChange={setAssetName}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The symbol that will represent this asset.')}>
|
||||
<Input
|
||||
defaultValue={initial?.assetSymbol}
|
||||
isError={!isValidSymbol}
|
||||
label={t('asset symbol')}
|
||||
onChange={setAssetSymbol}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The number of decimals for this token. Max allowed via the UI is set to 20.')}>
|
||||
<InputNumber
|
||||
defaultValue={initial?.assetDecimals}
|
||||
isError={!isValidDecimals}
|
||||
label={t('asset decimals')}
|
||||
onChange={setAssetDecimals}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The minimum balance for the asset. This is specified in the units and decimals as requested.')}>
|
||||
<InputBalance
|
||||
defaultValue={initial?.minBalance}
|
||||
isZeroable={false}
|
||||
label={t('minimum balance')}
|
||||
onChange={setMinBalance}
|
||||
siDecimals={siDecimals}
|
||||
siSymbol={siSymbol}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The selected id for the asset. This should not match an already-existing asset id.')}>
|
||||
<InputNumber
|
||||
bitLength={ASSET_ID_BIT_LENGTH}
|
||||
defaultValue={initial?.assetId || initialId}
|
||||
isError={!isValidId}
|
||||
isZeroable={false}
|
||||
label={t('asset id')}
|
||||
onChange={setAssetId}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Info);
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { TeamState } from './types.js';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { InputAddress, Modal } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
|
||||
interface Props {
|
||||
accountId: string;
|
||||
className?: string;
|
||||
defaultValue: TeamState | null;
|
||||
onChange: (info: TeamState | null) => void;
|
||||
}
|
||||
|
||||
function Team ({ accountId, className = '', defaultValue, onChange }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [initial] = useState(() => defaultValue);
|
||||
const [adminId, setAdminId] = useState<string | null>(null);
|
||||
const [freezerId, setFreezerId] = useState<string | null>(null);
|
||||
const [issuerId, setIssuerId] = useState<string | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
onChange(
|
||||
adminId && freezerId && issuerId
|
||||
? { adminId, freezerId, issuerId }
|
||||
: null
|
||||
);
|
||||
}, [api, adminId, freezerId, issuerId, onChange]);
|
||||
|
||||
return (
|
||||
<Modal.Content className={className}>
|
||||
<Modal.Columns hint={t('The account that is to be used for ongoing admin on the token.')}>
|
||||
<InputAddress
|
||||
defaultValue={initial?.adminId || accountId}
|
||||
label={t('admin account')}
|
||||
onChange={setAdminId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The account that is to be used for issuing this token.')}>
|
||||
<InputAddress
|
||||
defaultValue={initial?.issuerId || accountId}
|
||||
label={t('issuer account')}
|
||||
onChange={setIssuerId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The account that is to be used for performing freezing.')}>
|
||||
<InputAddress
|
||||
defaultValue={initial?.freezerId || accountId}
|
||||
label={t('freezer account')}
|
||||
onChange={setFreezerId}
|
||||
type='account'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Team);
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import Create from './Create.js';
|
||||
|
||||
interface Props {
|
||||
assetIds?: BN[];
|
||||
className?: string;
|
||||
openId: BN;
|
||||
}
|
||||
|
||||
function CreateButton ({ assetIds, className, openId }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={!assetIds || !hasAccounts}
|
||||
label={t('Create')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && assetIds && (
|
||||
<Create
|
||||
assetIds={assetIds}
|
||||
className={className}
|
||||
onClose={toggleOpen}
|
||||
openId={openId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(CreateButton);
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
export interface InfoState {
|
||||
accountId: string;
|
||||
assetDecimals: BN;
|
||||
assetId: BN;
|
||||
assetName: string;
|
||||
assetSymbol: string;
|
||||
minBalance: BN;
|
||||
}
|
||||
|
||||
export interface TeamState {
|
||||
adminId: string;
|
||||
issuerId: string;
|
||||
freezerId: string;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletAssetsAssetDetails, PalletAssetsAssetMetadata } from '@pezkuwi/types/lookup';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { InputAddress, InputBalance, Modal, TxButton } from '@pezkuwi/react-components';
|
||||
import { useApi } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
details: PalletAssetsAssetDetails;
|
||||
id: BN;
|
||||
metadata: PalletAssetsAssetMetadata;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function Mint ({ className, details: { issuer, minBalance }, id, metadata, onClose }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const [amount, setAmount] = useState<BN | undefined>();
|
||||
const [recipientId, setRecipientId] = useState<string | null>(null);
|
||||
|
||||
const isAmountValid = useMemo(
|
||||
() => amount && amount.gte(minBalance),
|
||||
[amount, minBalance]
|
||||
);
|
||||
|
||||
const [siDecimals, siSymbol] = useMemo(
|
||||
() => [metadata.decimals.toNumber(), metadata.symbol.toUtf8().toUpperCase()],
|
||||
[metadata]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('mint asset')}
|
||||
onClose={onClose}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Columns hint={t('The recipient account for this minting operation.')}>
|
||||
<InputAddress
|
||||
defaultValue={issuer}
|
||||
isDisabled
|
||||
label={t('issuer account')}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The recipient account for this minting operation.')}>
|
||||
<InputAddress
|
||||
label={t('mint to address')}
|
||||
onChange={setRecipientId}
|
||||
type='allPlus'
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The amount of tokens to issue to the account.')}>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
isError={!isAmountValid}
|
||||
isZeroable={false}
|
||||
label={t('amount to issue')}
|
||||
onChange={setAmount}
|
||||
siDecimals={siDecimals}
|
||||
siSymbol={siSymbol}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns hint={t('The minimum balance allowed for the asset.')}>
|
||||
<InputBalance
|
||||
defaultValue={minBalance}
|
||||
isDisabled
|
||||
label={t('minimum balance')}
|
||||
siDecimals={siDecimals}
|
||||
siSymbol={siSymbol}
|
||||
/>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<TxButton
|
||||
accountId={issuer}
|
||||
icon='plus'
|
||||
isDisabled={!recipientId || !isAmountValid}
|
||||
label={t('Mint')}
|
||||
onStart={onClose}
|
||||
params={[id, recipientId, amount]}
|
||||
tx={api.tx.assets.mint}
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Mint);
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { PalletAssetsAssetDetails, PalletAssetsAssetMetadata } from '@pezkuwi/types/lookup';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
import { useToggle } from '@pezkuwi/react-hooks';
|
||||
|
||||
import { useTranslation } from '../../translate.js';
|
||||
import Modal from './Mint.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
details: PalletAssetsAssetDetails;
|
||||
id: BN;
|
||||
metadata: PalletAssetsAssetMetadata;
|
||||
}
|
||||
|
||||
function Mint ({ className, details, id, metadata }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, toggleOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
icon='plus'
|
||||
isDisabled={metadata.isFrozen.isTrue}
|
||||
label={t('Mint')}
|
||||
onClick={toggleOpen}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Modal
|
||||
className={className}
|
||||
details={details}
|
||||
id={id}
|
||||
metadata={metadata}
|
||||
onClose={toggleOpen}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Mint);
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-explorer authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { Button, FilterOverlay, Input, styled } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onQuery?: (value: string) => void;
|
||||
}
|
||||
|
||||
function Query ({ className = '', onQuery }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const _onChange = useCallback(
|
||||
(value: string): void => setInput(value),
|
||||
[]
|
||||
);
|
||||
|
||||
const _onQuery = useCallback(
|
||||
(): void => {
|
||||
onQuery && onQuery(input);
|
||||
},
|
||||
[input, onQuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledFilterOverlay className={`${className} ui--FilterOverlay hasOwnMaxWidth`}>
|
||||
<Input
|
||||
className='asset--query'
|
||||
onChange={_onChange}
|
||||
onEnter={_onQuery}
|
||||
placeholder={t('asset id or name to query')}
|
||||
withLabel={false}
|
||||
>
|
||||
<Button
|
||||
icon='play'
|
||||
onClick={_onQuery}
|
||||
/>
|
||||
</Input>
|
||||
</StyledFilterOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledFilterOverlay = styled(FilterOverlay)`
|
||||
.asset--query {
|
||||
width: 20em;
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Query);
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets 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;
|
||||
numAssets?: number;
|
||||
}
|
||||
|
||||
function Summary ({ className, numAssets }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t('assets')}>
|
||||
{formatNumber(numAssets)}
|
||||
</CardSummary>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfo } from '@pezkuwi/react-hooks/types';
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button } from '@pezkuwi/react-components';
|
||||
|
||||
import Create from './Create/index.js';
|
||||
import Assets from './Assets.js';
|
||||
import Query from './Query.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
ids?: BN[];
|
||||
infos?: AssetInfo[];
|
||||
openId: BN;
|
||||
}
|
||||
|
||||
function Overview ({ className, ids, infos, openId }: Props): React.ReactElement<Props> {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const filteredInfos = keyword ? infos?.filter(({ key, metadata }) => key === keyword || metadata?.name.toUtf8().includes(keyword)) : infos;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary numAssets={ids?.length} />
|
||||
<Query onQuery={setKeyword} />
|
||||
<Button.Group>
|
||||
<Create
|
||||
assetIds={ids}
|
||||
openId={openId}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Assets infos={filteredInfos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Overview);
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ForeignAssetInfo } from '../useForeignAssetInfos.js';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { AddressSmall, CopyButton, Expander, styled } from '@pezkuwi/react-components';
|
||||
import { FormatBalance } from '@pezkuwi/react-query';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
value: ForeignAssetInfo;
|
||||
}
|
||||
|
||||
function Asset ({ className, value: { details, location, metadata } }: Props): React.ReactElement<Props> {
|
||||
const format = useMemo(
|
||||
(): [number, string] => metadata
|
||||
? [metadata.decimals.toNumber(), metadata.symbol.toUtf8()]
|
||||
: [0, '---'],
|
||||
[metadata]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<Location>
|
||||
<Expander
|
||||
isLeft
|
||||
summary={JSON.stringify(location?.toJSON()).substring(0, 40)}
|
||||
>
|
||||
<pre>
|
||||
{JSON.stringify(location?.toJSON(), null, 2)}
|
||||
</pre>
|
||||
</Expander>
|
||||
<CopyButton value={JSON.stringify(location?.toJSON(), null, 2)} />
|
||||
</Location>
|
||||
<td className='together'>{metadata?.name.toUtf8()}</td>
|
||||
<td className='address media--1000'>{details && <AddressSmall value={details.owner} />}</td>
|
||||
<td className='address media--1300'>{details && <AddressSmall value={details.admin} />}</td>
|
||||
<td className='address media--1600'>{details && <AddressSmall value={details.issuer} />}</td>
|
||||
<td className='address media--1900'>{details && <AddressSmall value={details.freezer} />}</td>
|
||||
<td className='number media--800'>{details && (
|
||||
<FormatBalance
|
||||
format={format}
|
||||
value={details.supply}
|
||||
/>
|
||||
)}</td>
|
||||
<td className='number all'>{details?.accounts.toString() || '0'}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const Location = styled.td`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
white-space: nowrap;
|
||||
font-style: italic;
|
||||
pre {
|
||||
overflow: visible;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.ui--Expander-summary svg {
|
||||
padding: 0.3rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--border-input);
|
||||
}
|
||||
`;
|
||||
|
||||
export default React.memo(Asset);
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ForeignAssetInfo } from '../useForeignAssetInfos.js';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { Table } from '@pezkuwi/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Asset from './Asset.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
infos?: ForeignAssetInfo[];
|
||||
}
|
||||
|
||||
function Assets ({ className, infos }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('foreign assets'), 'start'],
|
||||
[],
|
||||
[t('owner'), 'address media--1000'],
|
||||
[t('admin'), 'address media--1300'],
|
||||
[t('issuer'), 'address media--1600'],
|
||||
[t('freezer'), 'address media--1900'],
|
||||
[t('supply'), 'media--800'],
|
||||
[t('holders')]
|
||||
]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
className={className}
|
||||
empty={infos && t('No assets found')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{infos?.map((info) => (
|
||||
<Asset
|
||||
key={info.key}
|
||||
value={info}
|
||||
/>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Assets);
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets 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;
|
||||
numAssets?: number;
|
||||
}
|
||||
|
||||
function Summary ({ className, numAssets }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SummaryBox className={className}>
|
||||
<CardSummary label={t('foreign assets')}>
|
||||
{formatNumber(numAssets)}
|
||||
</CardSummary>
|
||||
</SummaryBox>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Summary);
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StagingXcmV3MultiLocation } from '@pezkuwi/types/lookup';
|
||||
import type { ForeignAssetInfo } from '../useForeignAssetInfos.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Assets from './Assets.js';
|
||||
import Summary from './Summary.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
foreignAssetInfos?: ForeignAssetInfo[]
|
||||
locations?: StagingXcmV3MultiLocation[];
|
||||
}
|
||||
|
||||
function ForeignAssets ({ className, foreignAssetInfos, locations }: Props): React.ReactElement<Props> {
|
||||
return (
|
||||
<div className={className}>
|
||||
<Summary numAssets={locations?.length} />
|
||||
<Assets infos={foreignAssetInfos} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ForeignAssets);
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// augment package
|
||||
import '@pezkuwi/api-augment/bizinikiwi';
|
||||
|
||||
import type { BN } from '@pezkuwi/util';
|
||||
import type { HexString } from '@pezkuwi/util/types';
|
||||
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { Route, Routes } from 'react-router';
|
||||
|
||||
import { getGenesis } from '@pezkuwi/apps-config';
|
||||
import { Tabs } from '@pezkuwi/react-components';
|
||||
import { useAccounts, useApi, useAssetIds, useAssetInfos } from '@pezkuwi/react-hooks';
|
||||
import { BN_ONE } from '@pezkuwi/util';
|
||||
|
||||
import Balances from './Balances/index.js';
|
||||
import ForeignAssets from './foreignAssets/index.js';
|
||||
import Overview from './Overview/index.js';
|
||||
import { useTranslation } from './translate.js';
|
||||
import { useForeignAssetInfos } from './useForeignAssetInfos.js';
|
||||
import { useForeignAssetLocations } from './useForeignAssetLocations.js';
|
||||
|
||||
interface Props {
|
||||
basePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Chains in which next asset id should be incremented from 1
|
||||
const GENESIS_HASHES = [getGenesis('statemint'), getGenesis('statemine')];
|
||||
|
||||
function findOpenId (genesisHash: HexString, ids?: BN[]): BN {
|
||||
if (!ids?.length) {
|
||||
return BN_ONE;
|
||||
}
|
||||
|
||||
if (GENESIS_HASHES.includes(genesisHash)) {
|
||||
return ids.sort((a, b) => a.cmp(b))[ids.length - 1].add(BN_ONE);
|
||||
}
|
||||
|
||||
const lastTaken = ids.find((id, index) =>
|
||||
index === 0
|
||||
? !id.eq(BN_ONE)
|
||||
: !id.sub(BN_ONE).eq(ids[index - 1])
|
||||
);
|
||||
|
||||
return lastTaken
|
||||
? lastTaken.add(BN_ONE)
|
||||
: ids[ids.length - 1].add(BN_ONE);
|
||||
}
|
||||
|
||||
function AssetApp ({ basePath, className }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { api } = useApi();
|
||||
const { hasAccounts } = useAccounts();
|
||||
const ids = useAssetIds();
|
||||
const infos = useAssetInfos(ids);
|
||||
|
||||
const foreignAssetLocations = useForeignAssetLocations();
|
||||
const foreignAssetInfos = useForeignAssetInfos(foreignAssetLocations);
|
||||
|
||||
const tabsRef = useRef([
|
||||
{
|
||||
isRoot: true,
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
},
|
||||
{
|
||||
name: 'foreignAssets',
|
||||
text: t('Foreign assets')
|
||||
},
|
||||
{
|
||||
name: 'balances',
|
||||
text: t('Balances')
|
||||
}
|
||||
]);
|
||||
|
||||
const showForeignAssetsTab = useMemo(() => !!foreignAssetLocations.length, [foreignAssetLocations.length]);
|
||||
const showBalancesTab = useMemo(() => hasAccounts && infos && infos.some(({ details, metadata }) => !!(details && metadata)), [hasAccounts, infos]);
|
||||
|
||||
const hidden = useMemo(
|
||||
() =>
|
||||
[!showForeignAssetsTab && 'foreignAssets', !showBalancesTab && 'balances'].filter((a) => typeof a === 'string'),
|
||||
[showBalancesTab, showForeignAssetsTab]
|
||||
);
|
||||
|
||||
const openId = useMemo(
|
||||
() => findOpenId(
|
||||
api.genesisHash.toHex(),
|
||||
// Check if id is valid digit
|
||||
ids?.filter((id) => /^\d{1,3}(,\d{3})*$/.test(id.toString()))),
|
||||
[api.genesisHash, ids]
|
||||
);
|
||||
|
||||
return (
|
||||
<main className={className}>
|
||||
<Tabs
|
||||
basePath={basePath}
|
||||
hidden={hidden}
|
||||
items={tabsRef.current}
|
||||
/>
|
||||
<Routes>
|
||||
<Route path={basePath}>
|
||||
<Route
|
||||
element={
|
||||
<Balances infos={infos} />
|
||||
}
|
||||
path='balances'
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<ForeignAssets
|
||||
foreignAssetInfos={foreignAssetInfos}
|
||||
locations={foreignAssetLocations}
|
||||
/>
|
||||
}
|
||||
path='foreignAssets'
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<Overview
|
||||
ids={ids}
|
||||
infos={infos}
|
||||
openId={openId}
|
||||
/>
|
||||
}
|
||||
index
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(AssetApp);
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets 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-assets');
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2017-2025 @pezkuwi/react-hooks authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { Option } from '@pezkuwi/types';
|
||||
import type { AccountId } from '@pezkuwi/types/interfaces';
|
||||
import type { PalletAssetsAssetDetails, PalletAssetsAssetMetadata, StagingXcmV3MultiLocation } from '@pezkuwi/types/lookup';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { createNamedHook, useAccounts, useApi, useCall } from '@pezkuwi/react-hooks';
|
||||
|
||||
const EMPTY_FLAGS = {
|
||||
isAdminMe: false,
|
||||
isFreezerMe: false,
|
||||
isIssuerMe: false,
|
||||
isOwnerMe: false
|
||||
};
|
||||
|
||||
export interface ForeignAssetInfo {
|
||||
details: PalletAssetsAssetDetails | null;
|
||||
location: StagingXcmV3MultiLocation;
|
||||
isAdminMe: boolean;
|
||||
isIssuerMe: boolean;
|
||||
isFreezerMe: boolean;
|
||||
isOwnerMe: boolean;
|
||||
key: string;
|
||||
metadata: PalletAssetsAssetMetadata | null;
|
||||
}
|
||||
|
||||
const QUERY_OPTS = { withParams: true };
|
||||
|
||||
function isAccount (allAccounts: string[], accountId: AccountId): boolean {
|
||||
const address = accountId.toString();
|
||||
|
||||
return allAccounts.some((a) => a === address);
|
||||
}
|
||||
|
||||
function extractInfo (allAccounts: string[], location: StagingXcmV3MultiLocation, optDetails: Option<PalletAssetsAssetDetails>, metadata: PalletAssetsAssetMetadata): ForeignAssetInfo {
|
||||
const details = optDetails.unwrapOr(null);
|
||||
|
||||
return {
|
||||
...(details
|
||||
? {
|
||||
isAdminMe: isAccount(allAccounts, details.admin),
|
||||
isFreezerMe: isAccount(allAccounts, details.freezer),
|
||||
isIssuerMe: isAccount(allAccounts, details.issuer),
|
||||
isOwnerMe: isAccount(allAccounts, details.owner)
|
||||
}
|
||||
: EMPTY_FLAGS
|
||||
),
|
||||
details,
|
||||
key: String(location),
|
||||
location,
|
||||
metadata: metadata.isEmpty
|
||||
? null
|
||||
: metadata
|
||||
};
|
||||
}
|
||||
|
||||
function useForeignAssetInfosImpl (locations?: StagingXcmV3MultiLocation[]): ForeignAssetInfo[] | undefined {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const isReady = useMemo(() => !!locations?.length && !!api.tx.foreignAssets?.setMetadata && !!api.tx.foreignAssets?.transferKeepAlive, [api.tx.foreignAssets?.setMetadata, api.tx.foreignAssets?.transferKeepAlive, locations?.length]);
|
||||
|
||||
const metadata = useCall<[[StagingXcmV3MultiLocation[]], PalletAssetsAssetMetadata[]]>(isReady && api.query.foreignAssets.metadata.multi, [locations], QUERY_OPTS);
|
||||
const details = useCall<[[StagingXcmV3MultiLocation[]], Option<PalletAssetsAssetDetails>[]]>(isReady && api.query.foreignAssets.asset.multi, [locations], QUERY_OPTS);
|
||||
const [state, setState] = useState<ForeignAssetInfo[] | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
details && metadata && (details[0][0].length === metadata[0][0].length) &&
|
||||
setState(
|
||||
details[0][0].map((location, index) =>
|
||||
extractInfo(allAccounts, location, details[1][index], metadata[1][index])
|
||||
)
|
||||
);
|
||||
}, [allAccounts, details, locations, metadata]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export const useForeignAssetInfos = createNamedHook('useForeignAssetInfos', useForeignAssetInfosImpl);
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2017-2025 @pezkuwi/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { StorageKey } from '@pezkuwi/types';
|
||||
import type { StagingXcmV3MultiLocation } from '@pezkuwi/types/lookup';
|
||||
|
||||
import { createNamedHook, useApi, useMapKeys } from '@pezkuwi/react-hooks';
|
||||
|
||||
const EMPTY_PARAMS: unknown[] = [];
|
||||
|
||||
const OPT_KEY = {
|
||||
transform: (keys: StorageKey<[StagingXcmV3MultiLocation]>[]): StagingXcmV3MultiLocation[] =>
|
||||
keys.flatMap(({ args }) => args)
|
||||
};
|
||||
|
||||
function useForeignAssetLocationsImpl () {
|
||||
const { api, isApiReady } = useApi();
|
||||
|
||||
const values = useMapKeys(isApiReady && api.query.foreignAssets?.asset, EMPTY_PARAMS, OPT_KEY) || [];
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export const useForeignAssetLocations = createNamedHook('useForeignAssetLocations', useForeignAssetLocationsImpl);
|
||||
Reference in New Issue
Block a user