Zilliqa chain type: support for ZRC-2 (#3202)

* Zilliqa chain type: support for ZRC-2

* add zrc-2 address

* add socket support and token select info

* add token types to watchlist notification settings

* add token tag

* add is fungible helper

* aboba

* review fixes
This commit is contained in:
Igor Stuev
2026-01-05 17:18:37 +01:00
committed by GitHub
parent 48df793a6e
commit fa5dbe232a
47 changed files with 450 additions and 182 deletions
+2
View File
@@ -1,4 +1,5 @@
import type { RollupType } from 'types/client/rollup';
import type { AdditionalTokenType } from 'types/client/token';
import type { NetworkVerificationType, NetworkVerificationTypeEnvs } from 'types/networks';
import { urlValidator } from 'toolkit/components/forms/validators/url';
@@ -48,6 +49,7 @@ const chain = Object.freeze({
},
hasMultipleGasCurrencies: getEnvValue('NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES') === 'true',
tokenStandard: getEnvValue('NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME') || 'ERC',
additionalTokenTypes: parseEnvJson<Array<AdditionalTokenType>>(getEnvValue('NEXT_PUBLIC_NETWORK_ADDITIONAL_TOKEN_TYPES')) || [],
rpcUrls,
isTestnet: getEnvValue('NEXT_PUBLIC_IS_TESTNET') === 'true',
verificationType,
+2 -1
View File
@@ -51,4 +51,5 @@ NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=zil
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=["base16", "bech32"]
NEXT_PUBLIC_VIEWS_CONTRACT_DECODED_BYTECODE_ENABLED=true
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_NETWORK_ADDITIONAL_TOKEN_TYPES=[{'id':'ZRC-2','name':'ZRC-2'}]
@@ -42,5 +42,13 @@ export default yup.object({
otherwise: (schema) => schema,
}),
NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME: yup.string(),
NEXT_PUBLIC_NETWORK_ADDITIONAL_TOKEN_TYPES: yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.object({
id: yup.string().required(),
name: yup.string().required(),
}).noUnknown(true)),
NEXT_PUBLIC_IS_TESTNET: yup.boolean(),
});
+1
View File
@@ -119,6 +119,7 @@ Also, be aware that if you customize the name of the currency or any of its deno
| NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES | `boolean` | Set to `true` for networks where users can pay transaction fees in either the native coin or ERC-20 tokens. | - | `false` | `true` | v1.33.0+ |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` \| `mining` \| 'fee reception' | Verification type in the network. Irrelevant for Arbitrum (verification type is always `posting`) and ZkEvm (verification type is always `sequencing`) L2s | - | `mining` | `validation` | v1.0.x+ |
| NEXT_PUBLIC_NETWORK_TOKEN_STANDARD_NAME | `string` | Name of the standard for creating tokens | - | `ERC` | `BEP` | v1.31.0+ |
| NEXT_PUBLIC_NETWORK_ADDITIONAL_TOKEN_TYPES | `Array<{id: string; name: string;}>` | List of additional **ERC-20-like (fungible)** token types supported by the explorer UI (extends token type filters/labels). *Note: the token type id must also be supported by the backend API responses and filters.* | - | `[]` | `'[{"id":"ERC-777","name":"ERC-777"}]'` | v2.6.0+ |
| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | v1.0.x+ |
&nbsp;
+26 -6
View File
@@ -3,21 +3,41 @@ import type { NFTTokenType, TokenType } from 'types/api/token';
import config from 'configs/app';
const tokenStandardName = config.chain.tokenStandard;
const additionalTokenTypes = config.chain.additionalTokenTypes;
export const NFT_TOKEN_TYPES: Record<NFTTokenType, string > = {
export const NFT_TOKEN_TYPES: Record<NFTTokenType, string> = {
'ERC-721': `${ tokenStandardName }-721`,
'ERC-1155': `${ tokenStandardName }-1155`,
'ERC-404': `${ tokenStandardName }-404`,
};
export const TOKEN_TYPES: Record<TokenType, string > = {
export const TOKEN_TYPES: Record<string, string> = {
'ERC-20': `${ tokenStandardName }-20`,
...additionalTokenTypes.reduce((result, item) => {
result[item.id] = item.name;
return result;
}, {} as Record<string, string>),
...NFT_TOKEN_TYPES,
};
export const NFT_TOKEN_TYPE_IDS: Array<NFTTokenType> = [ 'ERC-721', 'ERC-1155', 'ERC-404' ];
export const TOKEN_TYPE_IDS: Array<TokenType> = [ 'ERC-20', ...NFT_TOKEN_TYPE_IDS ];
export const NFT_TOKEN_TYPE_IDS: Array<NFTTokenType> = Object.keys(NFT_TOKEN_TYPES) as Array<NFTTokenType>;
export const TOKEN_TYPE_IDS = Object.keys(TOKEN_TYPES);
export function getTokenTypeName(typeId: TokenType) {
return TOKEN_TYPES[typeId];
export function getTokenTypeName(typeId: string) {
return TOKEN_TYPES[typeId] || typeId;
}
export function isFungibleTokenType(typeId: TokenType): boolean {
return typeId === 'ERC-20' || additionalTokenTypes.some((item) => item.id === typeId);
}
export function hasTokenTransferValue(typeId: TokenType) {
if (typeId === 'ERC-20' || typeId === 'ERC-1155' || typeId === 'ERC-404') {
return true;
}
return additionalTokenTypes.some((item) => item.id === typeId);
}
export function hasTokenIds(typeId: TokenType) {
return typeId === 'ERC-1155' || typeId === 'ERC-404';
}
+30 -30
View File
@@ -8,7 +8,7 @@ export const tokenInfo: TokenInfo = {
holders_count: '46554',
name: 'ARIANEE',
symbol: 'ARIA',
type: 'ERC-20' as const,
type: 'ERC-20',
total_supply: '1235',
icon_url: 'http://localhost:3000/token-icon.png',
reputation: 'ok',
@@ -19,7 +19,7 @@ export const tokenCounters: TokenCounters = {
transfers_count: '88282281',
};
export const tokenInfoERC20a: TokenInfo<'ERC-20'> = {
export const tokenInfoERC20a: TokenInfo = {
address_hash: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
circulating_market_cap: '117268489.23970924',
decimals: '18',
@@ -28,12 +28,12 @@ export const tokenInfoERC20a: TokenInfo<'ERC-20'> = {
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20' as const,
type: 'ERC-20',
icon_url: 'http://localhost:3000/token-icon.png',
reputation: 'ok',
};
export const tokenInfoERC20b: TokenInfo<'ERC-20'> = {
export const tokenInfoERC20b: TokenInfo = {
address_hash: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
circulating_market_cap: '115060192.36105014',
decimals: '6',
@@ -42,12 +42,12 @@ export const tokenInfoERC20b: TokenInfo<'ERC-20'> = {
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20' as const,
type: 'ERC-20',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC20c: TokenInfo<'ERC-20'> = {
export const tokenInfoERC20c: TokenInfo = {
address_hash: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A8',
circulating_market_cap: null,
decimals: '18',
@@ -56,12 +56,12 @@ export const tokenInfoERC20c: TokenInfo<'ERC-20'> = {
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20' as const,
type: 'ERC-20',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC20d: TokenInfo<'ERC-20'> = {
export const tokenInfoERC20d: TokenInfo = {
address_hash: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9196',
circulating_market_cap: null,
decimals: '18',
@@ -70,12 +70,12 @@ export const tokenInfoERC20d: TokenInfo<'ERC-20'> = {
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20' as const,
type: 'ERC-20',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = {
export const tokenInfoERC20LongSymbol: TokenInfo = {
address_hash: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9197',
circulating_market_cap: '112855875.75888918',
decimals: '18',
@@ -84,12 +84,12 @@ export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = {
name: 'Zeta',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
total_supply: '2100000000000000000000000000',
type: 'ERC-20' as const,
type: 'ERC-20',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC721a: TokenInfo<'ERC-721'> = {
export const tokenInfoERC721a: TokenInfo = {
address_hash: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
circulating_market_cap: null,
decimals: null,
@@ -98,12 +98,12 @@ export const tokenInfoERC721a: TokenInfo<'ERC-721'> = {
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721' as const,
type: 'ERC-721',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC721b: TokenInfo<'ERC-721'> = {
export const tokenInfoERC721b: TokenInfo = {
address_hash: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
circulating_market_cap: null,
decimals: null,
@@ -112,12 +112,12 @@ export const tokenInfoERC721b: TokenInfo<'ERC-721'> = {
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721' as const,
type: 'ERC-721',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC721c: TokenInfo<'ERC-721'> = {
export const tokenInfoERC721c: TokenInfo = {
address_hash: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
circulating_market_cap: null,
decimals: null,
@@ -126,12 +126,12 @@ export const tokenInfoERC721c: TokenInfo<'ERC-721'> = {
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721' as const,
type: 'ERC-721',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = {
export const tokenInfoERC721LongSymbol: TokenInfo = {
address_hash: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4993',
circulating_market_cap: null,
decimals: null,
@@ -140,12 +140,12 @@ export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = {
name: 'Puma',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
total_supply: null,
type: 'ERC-721' as const,
type: 'ERC-721',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = {
export const tokenInfoERC1155a: TokenInfo = {
address_hash: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
circulating_market_cap: null,
decimals: null,
@@ -154,12 +154,12 @@ export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = {
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155' as const,
type: 'ERC-1155',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = {
export const tokenInfoERC1155b: TokenInfo = {
address_hash: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
circulating_market_cap: null,
decimals: null,
@@ -168,12 +168,12 @@ export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = {
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155' as const,
type: 'ERC-1155',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = {
export const tokenInfoERC1155WithoutName: TokenInfo = {
address_hash: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8a',
circulating_market_cap: null,
decimals: null,
@@ -182,12 +182,12 @@ export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = {
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155' as const,
type: 'ERC-1155',
icon_url: null,
reputation: 'ok',
};
export const tokenInfoERC404: TokenInfo<'ERC-404'> = {
export const tokenInfoERC404: TokenInfo = {
address_hash: '0xB5C457dDB4cE3312a6C5a2b056a1652bd542a208',
circulating_market_cap: '0.0',
decimals: '18',
@@ -197,11 +197,11 @@ export const tokenInfoERC404: TokenInfo<'ERC-404'> = {
name: 'OMNI404',
symbol: 'O404',
total_supply: '6482275000000000000',
type: 'ERC-404' as const,
type: 'ERC-404',
reputation: 'ok',
};
export const bridgedTokenA: TokenInfo<'ERC-20'> = {
export const bridgedTokenA: TokenInfo = {
...tokenInfoERC20a,
is_bridged: true,
origin_chain_id: '1',
@@ -209,7 +209,7 @@ export const bridgedTokenA: TokenInfo<'ERC-20'> = {
foreign_address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8b',
};
export const bridgedTokenB: TokenInfo<'ERC-20'> = {
export const bridgedTokenB: TokenInfo = {
...tokenInfoERC20b,
is_bridged: true,
origin_chain_id: '56',
@@ -217,7 +217,7 @@ export const bridgedTokenB: TokenInfo<'ERC-20'> = {
foreign_address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefd',
};
export const bridgedTokenC: TokenInfo<'ERC-20'> = {
export const bridgedTokenC: TokenInfo = {
...tokenInfoERC20d,
is_bridged: true,
origin_chain_id: '99',
+4 -4
View File
@@ -154,7 +154,7 @@ export const erc1155A: TokenTransfer = {
export const erc1155B: TokenTransfer = {
...erc1155A,
token: {
...(erc1155A.token as TokenInfo<'ERC-1155'>),
...(erc1155A.token as TokenInfo),
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
@@ -164,7 +164,7 @@ export const erc1155B: TokenTransfer = {
export const erc1155C: TokenTransfer = {
...erc1155A,
token: {
...(erc1155A.token as TokenInfo<'ERC-1155'>),
...(erc1155A.token as TokenInfo),
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
@@ -174,7 +174,7 @@ export const erc1155C: TokenTransfer = {
export const erc1155D: TokenTransfer = {
...erc1155A,
token: {
...(erc1155A.token as TokenInfo<'ERC-1155'>),
...(erc1155A.token as TokenInfo),
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
@@ -235,7 +235,7 @@ export const erc404A: TokenTransfer = {
export const erc404B: TokenTransfer = {
...erc404A,
token: {
...(erc404A.token as TokenInfo<'ERC-404'>),
...(erc404A.token as TokenInfo),
name: 'SastanaNFT',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
},
+1 -4
View File
@@ -60,10 +60,7 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_20', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_721', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_1155', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'updated_token_balances_erc_404', payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: `updated_token_balances_${ string }`, payload: AddressTokensBalancesSocketMessage): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
+4 -4
View File
@@ -16,7 +16,7 @@ import { generateListStub } from './utils';
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
export const TOKEN_INFO_ERC_20: TokenInfo = {
address_hash: ADDRESS_HASH,
circulating_market_cap: '117629601.61913824',
decimals: '18',
@@ -30,19 +30,19 @@ export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
reputation: 'ok',
};
export const TOKEN_INFO_ERC_721: TokenInfo<'ERC-721'> = {
export const TOKEN_INFO_ERC_721: TokenInfo = {
...TOKEN_INFO_ERC_20,
circulating_market_cap: null,
type: 'ERC-721',
};
export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = {
export const TOKEN_INFO_ERC_1155: TokenInfo = {
...TOKEN_INFO_ERC_20,
circulating_market_cap: null,
type: 'ERC-1155',
};
export const TOKEN_INFO_ERC_404: TokenInfo<'ERC-404'> = {
export const TOKEN_INFO_ERC_404: TokenInfo = {
...TOKEN_INFO_ERC_20,
circulating_market_cap: null,
type: 'ERC-404',
+1
View File
@@ -39,6 +39,7 @@ export interface NotificationSettings {
'ERC-20': NotificationDirection;
'ERC-721': NotificationDirection;
'ERC-404': NotificationDirection;
[key: string]: NotificationDirection;
}
export interface NotificationMethods {
+1 -1
View File
@@ -144,7 +144,7 @@ export type AddressTokenTransferFilters = {
};
export type AddressTokensFilter = {
type: TokenType;
type: TokenType | Array<TokenType>;
};
export type AddressNFTTokensFilter = {
+2 -2
View File
@@ -41,7 +41,7 @@ export type CeloEpochDetails = {
carbon_offsetting_transfer: TokenTransfer | null;
community_transfer: TokenTransfer | null;
transfers_total: {
token: TokenInfo<'ERC-20'> | null;
token: TokenInfo | null;
total: Erc20TotalPayload | null;
} | null;
} | null;
@@ -50,7 +50,7 @@ export type CeloEpochDetails = {
export interface CeloEpochElectionReward {
count: number;
token: TokenInfo<'ERC-20'>;
token: TokenInfo;
total: string;
}
+5 -3
View File
@@ -2,13 +2,14 @@ import type { TokenInfoApplication } from './account';
import type { AddressParam } from './addressParams';
export type NFTTokenType = 'ERC-721' | 'ERC-1155' | 'ERC-404';
export type TokenType = 'ERC-20' | NFTTokenType;
// token type can come from the environment config, so it can be any string
export type TokenType = string;
export type TokenReputation = 'ok' | 'scam';
export interface TokenInfo<T extends TokenType = TokenType> {
export interface TokenInfo {
address_hash: string;
type: T;
type: TokenType;
symbol: string | null;
name: string | null;
decimals: string | null;
@@ -24,6 +25,7 @@ export interface TokenInfo<T extends TokenType = TokenType> {
origin_chain_id?: string | null;
foreign_address?: string | null;
filecoin_robust_address?: string | null;
zilliqa?: { zrc2_address_hash?: string };
}
export interface TokenCounters {
+4 -4
View File
@@ -30,19 +30,19 @@ export type Erc404TotalPayload = {
export type TokenTransfer = (
{
token: TokenInfo<'ERC-20'> | null;
token: TokenInfo | null;
total: Erc20TotalPayload | null;
} |
{
token: TokenInfo<'ERC-721'> | null;
token: TokenInfo | null;
total: Erc721TotalPayload | null;
} |
{
token: TokenInfo<'ERC-1155'> | null;
token: TokenInfo | null;
total: Erc1155TotalPayload | null;
} |
{
token: TokenInfo<'ERC-404'> | null;
token: TokenInfo | null;
total: Erc404TotalPayload | null;
}
) & TokenTransferBase;
+1 -1
View File
@@ -82,7 +82,7 @@ export type Transaction = {
};
// Celo fields
celo?: {
gas_token: TokenInfo<'ERC-20'> | null;
gas_token: TokenInfo | null;
};
// zkEvm fields
zkevm_verify_hash?: string;
+4 -4
View File
@@ -21,13 +21,13 @@ type ChangeDirection = 'from' | 'to';
export interface TxStateChangeTokenErc20 {
type: 'token';
token: TokenInfo<'ERC-20'>;
token: TokenInfo;
change: string;
}
export interface TxStateChangeTokenErc721 {
type: 'token';
token: TokenInfo<'ERC-721'>;
token: TokenInfo;
change: Array<{
direction: ChangeDirection;
total: Erc721TotalPayload;
@@ -36,14 +36,14 @@ export interface TxStateChangeTokenErc721 {
export interface TxStateChangeTokenErc1155 {
type: 'token';
token: TokenInfo<'ERC-1155'>;
token: TokenInfo;
change: string;
token_id: string;
}
export interface TxStateChangeTokenErc404 {
type: 'token';
token: TokenInfo<'ERC-404'>;
token: TokenInfo;
change: string;
token_id: string;
}
+5
View File
@@ -22,3 +22,8 @@ export interface TokenBridge {
title: string;
short_title: string;
}
export interface AdditionalTokenType {
id: string;
name: string;
}
+72 -1
View File
@@ -1,7 +1,7 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
import type { AddressTokenBalance, AddressTokensResponse } from 'types/api/address';
import * as addressMock from 'mocks/address/address';
import * as tokensMock from 'mocks/address/tokens';
@@ -268,6 +268,77 @@ test.describe('update balances via socket', () => {
await expect(component).toHaveScreenshot();
});
test('custom token ZRC-2', async({ render, page, createSocket, mockApiResponse, mockEnvs }) => {
test.slow();
await mockEnvs([
[ 'NEXT_PUBLIC_NETWORK_ADDITIONAL_TOKEN_TYPES', '[{"id":"ZRC-2","name":"ZRC-2"}]' ],
]);
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const responseErc20AndZrc2 = {
items: [ tokensMock.erc20a ],
next_page_params: null,
};
const responseZrc2 = {
items: [],
next_page_params: null,
};
const erc20AndZrc2ApiUrl = await mockApiResponse(
'general:address_tokens',
responseErc20AndZrc2,
{ pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [ 'ERC-20', 'ZRC-2' ] } },
);
const zrc2ApiUrl = await mockApiResponse(
'general:address_tokens',
responseZrc2,
{ pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ZRC-2' } },
);
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</Box>,
{ hooksConfig },
{ withSocket: true },
);
await page.waitForResponse(erc20AndZrc2ApiUrl);
await page.waitForResponse(zrc2ApiUrl);
await expect(component).toHaveScreenshot();
const zrc2TokenBalance: AddressTokenBalance = {
...tokensMock.erc20a,
token: {
...tokensMock.erc20a.token,
// custom token type enabled via NEXT_PUBLIC_NETWORK_ADDITIONAL_TOKEN_TYPES
type: 'ZRC-2',
address_hash: '0x00000000000000000000000000000000000000f2',
name: 'ZRC-2 Token',
symbol: 'ZRC2',
exchange_rate: '0.02',
} as unknown as AddressTokenBalance['token'],
value: '1234500000000000000',
};
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ ADDRESS_HASH.toLowerCase() }`);
socketServer.sendMessage(socket, channel, 'updated_token_balances_zrc_2', {
overflow: false,
token_balances: [ zrc2TokenBalance ],
});
await expect(component).toHaveScreenshot();
});
});
test('native token', async({ render, mockEnvs }) => {
+5 -2
View File
@@ -50,7 +50,7 @@ const AddressTokens = ({ shouldRender = true, isQueryEnabled = true }: Props) =>
const erc20Query = useQueryWithPages({
resourceName: 'general:address_tokens',
pathParams: { hash },
filters: { type: 'ERC-20' },
filters: { type: [ 'ERC-20', ...config.chain.additionalTokenTypes.map(item => item.id) ] },
scrollRef,
options: {
enabled: isQueryEnabled && (!tab || tab === 'tokens' || tab === 'tokens_erc20'),
@@ -74,7 +74,10 @@ const AddressTokens = ({ shouldRender = true, isQueryEnabled = true }: Props) =>
const tabs = [
{
id: 'tokens_erc20',
title: `${ config.chain.tokenStandard }-20`,
title: [
`${ config.chain.tokenStandard }-20`,
...config.chain.additionalTokenTypes.map((item) => item.name),
].join(' & '),
component: (
<ERC20Tokens
items={ erc20Query.data?.items }
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+15 -11
View File
@@ -6,6 +6,7 @@ import { route } from 'nextjs/routes';
import config from 'configs/app';
import multichainConfig from 'configs/multichain';
import { isFungibleTokenType } from 'lib/token/tokenTypes';
import { Link } from 'toolkit/chakra/link';
import { TruncatedText } from 'toolkit/components/truncation/TruncatedText';
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
@@ -34,18 +35,21 @@ const TokenSelectItem = ({ data }: Props) => {
}, [ data.chain_values ]);
const secondRow = (() => {
switch (data.token.type) {
case 'ERC-20': {
const tokenDecimals = Number(data.token.decimals ?? 18);
const text = `${ BigNumber(data.value).dividedBy(10 ** tokenDecimals).dp(8).toFormat() } ${ data.token.symbol || '' }`;
const isFungibleToken = isFungibleTokenType(data.token.type);
return (
<>
<TruncatedText text={ text }/>
{ data.token.exchange_rate && <chakra.span ml={ 2 }>@{ Number(data.token.exchange_rate).toLocaleString() }</chakra.span> }
</>
);
}
if (isFungibleToken) {
const tokenDecimals = Number(data.token.decimals ?? 18);
const text = `${ BigNumber(data.value).dividedBy(10 ** tokenDecimals).dp(8).toFormat() } ${ data.token.symbol || '' }`;
return (
<>
<TruncatedText text={ text }/>
{ data.token.exchange_rate && <chakra.span ml={ 2 }>@{ Number(data.token.exchange_rate).toLocaleString() }</chakra.span> }
</>
);
}
switch (data.token.type) {
case 'ERC-721': {
const text = `${ BigNumber(data.value).toFormat() } ${ data.token.symbol || '' }`;
return <TruncatedText text={ text }/>;
+10 -26
View File
@@ -3,7 +3,6 @@ import { sumBy } from 'es-toolkit';
import React from 'react';
import type { FormattedData } from './types';
import type { TokenType } from 'types/api/token';
import { getTokenTypeName } from 'lib/token/tokenTypes';
import { Link } from 'toolkit/chakra/link';
@@ -12,20 +11,18 @@ import { thinsp } from 'toolkit/utils/htmlEntities';
import IconSvg from 'ui/shared/IconSvg';
import type { Sort } from '../utils/tokenUtils';
import { sortTokenGroups, sortingFns } from '../utils/tokenUtils';
import { getSortingFn, sortTokenGroups } from '../utils/tokenUtils';
import TokenSelectItem from './TokenSelectItem';
interface Props {
searchTerm: string;
erc20sort: Sort;
erc1155sort: Sort;
erc404sort: Sort;
getSort: (typeId: string) => Sort;
filteredData: FormattedData;
onInputChange: (searchTerm: string) => void;
onSortClick: (event: React.SyntheticEvent) => void;
}
const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const TokenSelectMenu = ({ getSort, filteredData, onInputChange, onSortClick, searchTerm }: Props) => {
const hasFilteredResult = sumBy(Object.values(filteredData), ({ items }) => items.length) > 0;
return (
@@ -43,27 +40,14 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onI
return null;
}
const type = tokenType as TokenType;
const arrowTransform =
(type === 'ERC-1155' && erc1155sort === 'desc') ||
(type === 'ERC-404' && erc404sort === 'desc') ||
(type === 'ERC-20' && erc20sort === 'desc') ?
'rotate(90deg)' :
'rotate(-90deg)';
const sortDirection: Sort = (() => {
switch (type) {
case 'ERC-1155':
return erc1155sort;
case 'ERC-20':
return erc20sort;
default:
return 'desc';
}
})();
const hasSort =
const type = tokenType;
const sortDirection = getSort(type);
const arrowTransform = sortDirection === 'desc' ? 'rotate(90deg)' : 'rotate(-90deg)';
const hasSort = tokenInfo.items.length > 1 && (
(type === 'ERC-404' && tokenInfo.items.some(item => item.value)) ||
type === 'ERC-1155' ||
(type === 'ERC-20' && tokenInfo.items.some(({ usd }) => usd));
(type !== 'ERC-721' && tokenInfo.items.some(({ usd }) => usd))
);
const numPrefix = tokenInfo.isOverflow ? `>${ thinsp }` : '';
return (
@@ -78,7 +62,7 @@ const TokenSelectMenu = ({ erc20sort, erc1155sort, erc404sort, filteredData, onI
</Link>
) }
</Flex>
{ tokenInfo.items.sort(sortingFns[type](sortDirection)).map((data) =>
{ tokenInfo.items.sort(getSortingFn(type)(sortDirection)).map((data) =>
<TokenSelectItem key={ data.token.address_hash + data.token_id } data={ data }/>) }
</Box>
);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

+1 -3
View File
@@ -1,8 +1,6 @@
import type { TokenType } from 'types/api/token';
import type { TokenEnhancedData } from 'ui/address/utils/tokenUtils';
export type FormattedData = Record<TokenType, FormattedDataItem>;
export type FormattedData = Record<string, FormattedDataItem>;
export interface FormattedDataItem {
items: Array<TokenEnhancedData>;
+23 -4
View File
@@ -10,7 +10,7 @@ export default function useTokenSelect(data: FormattedData) {
const [ searchTerm, setSearchTerm ] = React.useState('');
const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc');
const [ erc404sort, setErc404Sort ] = React.useState<Sort>('desc');
const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc');
const [ fungibleSortByType, setFungibleSortByType ] = React.useState<Record<string, Sort>>({});
const onInputChange = React.useCallback((searchTerm: string) => {
setSearchTerm(searchTerm);
@@ -20,13 +20,22 @@ export default function useTokenSelect(data: FormattedData) {
const tokenType = (event.currentTarget as HTMLAnchorElement).getAttribute('data-type');
if (tokenType === 'ERC-1155') {
setErc1155Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
return;
}
if (tokenType === 'ERC-404') {
setErc404Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
return;
}
if (tokenType === 'ERC-20') {
setErc20Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
if (!tokenType) {
return;
}
setFungibleSortByType((prevValue) => {
const current = prevValue[tokenType] || 'desc';
return {
...prevValue,
[tokenType]: current === 'desc' ? 'asc' : 'desc',
};
});
}, []);
const filteredData = React.useMemo(() => {
@@ -36,9 +45,19 @@ export default function useTokenSelect(data: FormattedData) {
}));
}, [ data, searchTerm ]);
const getSort = React.useCallback((typeId: string): Sort => {
if (typeId === 'ERC-1155') {
return erc1155sort;
}
if (typeId === 'ERC-404') {
return erc404sort;
}
return fungibleSortByType[typeId] || 'desc';
}, [ erc1155sort, erc404sort, fungibleSortByType ]);
return {
searchTerm,
erc20sort,
getSort,
erc1155sort,
erc404sort,
onInputChange,
+11 -1
View File
@@ -4,6 +4,7 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import type { PaginationParams } from 'ui/shared/pagination/types';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
@@ -23,6 +24,8 @@ type Props = {
const ERC20Tokens = ({ items, isLoading, pagination, isError, top }: Props) => {
const isMobile = useIsMobile();
const hasAdditionalTokenTypes = config.chain.additionalTokenTypes.length > 0;
const actionBar = isMobile && pagination.isVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
@@ -31,12 +34,19 @@ const ERC20Tokens = ({ items, isLoading, pagination, isError, top }: Props) => {
const content = items ? (
<>
<Box hideBelow="lg"><ERC20TokensTable data={ items } top={ top ?? (pagination.isVisible ? 72 : 0) } isLoading={ isLoading }/></Box>
<Box hideBelow="lg">
<ERC20TokensTable
data={ items }
top={ top ?? (pagination.isVisible ? 72 : 0) }
isLoading={ isLoading }
hasAdditionalTokenTypes={ hasAdditionalTokenTypes }/>
</Box>
<Box hideFrom="lg">{ items.map((item, index) => (
<ERC20TokensListItem
key={ item.token.address_hash + (isLoading ? index : '') }
{ ...item }
isLoading={ isLoading }
hasAdditionalTokenTypes={ hasAdditionalTokenTypes }
/>
)) }
</Box>
+11 -3
View File
@@ -6,7 +6,9 @@ import type { AddressTokensErc20Item } from './types';
import config from 'configs/app';
import multichainConfig from 'configs/multichain';
import { getTokenTypeName } from 'lib/token/tokenTypes';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tag } from 'toolkit/chakra/tag';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
@@ -16,10 +18,15 @@ import calculateUsdValue from 'ui/shared/value/calculateUsdValue';
import SimpleValue from 'ui/shared/value/SimpleValue';
import { DEFAULT_ACCURACY_USD } from 'ui/shared/value/utils';
type Props = AddressTokensErc20Item & { isLoading: boolean };
const ERC20TokensListItem = ({ token, value, isLoading, chain_values: chainValues }: Props) => {
type Props = AddressTokensErc20Item & { isLoading: boolean; hasAdditionalTokenTypes?: boolean };
const ERC20TokensListItem = ({
token,
value,
isLoading,
chain_values: chainValues,
hasAdditionalTokenTypes,
}: Props) => {
const {
valueBn: tokenQuantity,
usdBn: tokenValue,
@@ -51,6 +58,7 @@ const ERC20TokensListItem = ({ token, value, isLoading, chain_values: chainValue
width="auto"
/>
{ isNativeToken && <NativeTokenTag/> }
{ hasAdditionalTokenTypes && <Tag loading={ isLoading }>{ getTokenTypeName(token.type) }</Tag> }
</Flex>
<Flex alignItems="center" pl={ 8 }>
<AddressEntity
+3 -1
View File
@@ -10,9 +10,10 @@ interface Props {
data: Array<AddressTokensErc20Item>;
top: number;
isLoading: boolean;
hasAdditionalTokenTypes?: boolean;
}
const ERC20TokensTable = ({ data, top, isLoading }: Props) => {
const ERC20TokensTable = ({ data, top, isLoading, hasAdditionalTokenTypes }: Props) => {
return (
<TableRoot>
<TableHeaderSticky top={ top }>
@@ -30,6 +31,7 @@ const ERC20TokensTable = ({ data, top, isLoading }: Props) => {
key={ item.token.address_hash + (isLoading ? index : '') + (item.chain_values ? Object.keys(item.chain_values).join(',') : '') }
{ ...item }
isLoading={ isLoading }
hasAdditionalTokenTypes={ hasAdditionalTokenTypes }
/>
)) }
</TableBody>
+12 -6
View File
@@ -6,7 +6,9 @@ import type { AddressTokensErc20Item } from './types';
import config from 'configs/app';
import multichainConfig from 'configs/multichain';
import { getTokenTypeName } from 'lib/token/tokenTypes';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import { Tag } from 'toolkit/chakra/tag';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import NativeTokenTag from 'ui/shared/celo/NativeTokenTag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
@@ -15,13 +17,14 @@ import calculateUsdValue from 'ui/shared/value/calculateUsdValue';
import SimpleValue from 'ui/shared/value/SimpleValue';
import { DEFAULT_ACCURACY_USD } from 'ui/shared/value/utils';
type Props = AddressTokensErc20Item & { isLoading: boolean };
type Props = AddressTokensErc20Item & { isLoading: boolean; hasAdditionalTokenTypes?: boolean };
const ERC20TokensTableItem = ({
token,
value,
chain_values: chainValues,
isLoading,
hasAdditionalTokenTypes,
}: Props) => {
const {
@@ -42,9 +45,11 @@ const ERC20TokensTableItem = ({
return chain;
}, [ chainValues ]);
const cellVerticalAlign = hasAdditionalTokenTypes ? 'top' : 'middle';
return (
<TableRow className="group" >
<TableCell verticalAlign="middle">
<TableCell verticalAlign={ cellVerticalAlign }>
<HStack gap={ 2 }>
<TokenEntity
token={ token }
@@ -57,8 +62,9 @@ const ERC20TokensTableItem = ({
/>
{ isNativeToken && <NativeTokenTag/> }
</HStack>
{ hasAdditionalTokenTypes && <Tag loading={ isLoading } mt={ 2 }>{ getTokenTypeName(token.type) }</Tag> }
</TableCell>
<TableCell verticalAlign="middle">
<TableCell verticalAlign={ cellVerticalAlign }>
<Flex alignItems="center" width="150px" justifyContent="space-between">
<AddressEntity
address={{ hash: token.address_hash }}
@@ -69,7 +75,7 @@ const ERC20TokensTableItem = ({
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading } opacity="0" _groupHover={{ opacity: 1 }}/>
</Flex>
</TableCell>
<TableCell isNumeric verticalAlign="middle">
<TableCell isNumeric verticalAlign={ cellVerticalAlign }>
{ token.exchange_rate ? (
<SimpleValue
value={ BigNumber(token.exchange_rate) }
@@ -79,14 +85,14 @@ const ERC20TokensTableItem = ({
/>
) : null }
</TableCell>
<TableCell isNumeric verticalAlign="middle">
<TableCell isNumeric verticalAlign={ cellVerticalAlign }>
<SimpleValue
value={ tokenQuantity }
color={ isNativeToken ? 'text.secondary' : undefined }
loading={ isLoading }
/>
</TableCell>
<TableCell isNumeric verticalAlign="middle">
<TableCell isNumeric verticalAlign={ cellVerticalAlign }>
{ token.exchange_rate && (
<SimpleValue
value={ tokenValue }
+29 -5
View File
@@ -2,10 +2,10 @@ import BigNumber from 'bignumber.js';
import { mapValues } from 'es-toolkit';
import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/token';
import config from 'configs/app';
import sumBnReducer from 'lib/bigint/sumBnReducer';
import { isFungibleTokenType } from 'lib/token/tokenTypes';
import { ZERO } from 'toolkit/utils/consts';
const isNativeToken = (token: TokenEnhancedData) =>
@@ -19,7 +19,7 @@ export type TokenEnhancedData = AddressTokenBalance & {
export type Sort = 'desc' | 'asc';
export type TokenSelectData = Record<TokenType, TokenSelectDataItem>;
export type TokenSelectData = Record<string, TokenSelectDataItem>;
export interface TokenSelectDataItem {
items: Array<TokenEnhancedData>;
@@ -28,10 +28,28 @@ export interface TokenSelectDataItem {
type TokenGroup = [string, TokenSelectDataItem];
const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155', 'ERC-404' ];
const NFT_TOKEN_GROUPS_ORDER = [ 'ERC-721', 'ERC-1155', 'ERC-404' ] as const;
export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => {
return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1;
const additionalTypeIds = config.chain.additionalTokenTypes.map((item) => item.id);
const tokenGroupsOrder = [
'ERC-20',
...additionalTypeIds,
...NFT_TOKEN_GROUPS_ORDER,
] as const;
const orderA = tokenGroupsOrder.indexOf(groupA[0] as (typeof tokenGroupsOrder)[number]);
const orderB = tokenGroupsOrder.indexOf(groupB[0] as (typeof tokenGroupsOrder)[number]);
const normalizedA = orderA === -1 ? 999 : orderA;
const normalizedB = orderB === -1 ? 999 : orderB;
if (normalizedA !== normalizedB) {
return normalizedA > normalizedB ? 1 : -1;
}
return groupA[0].localeCompare(groupB[0]);
};
const sortErc1155or404Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => {
@@ -74,6 +92,10 @@ export const sortingFns = {
'ERC-404': sortErc1155or404Tokens,
};
export const getSortingFn = (typeId: string) => {
return sortingFns[typeId as keyof typeof sortingFns] || sortErc20Tokens;
};
export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBalance) => {
if (!token.name) {
return !searchTerm ? true : token.address_hash.toLowerCase().includes(searchTerm);
@@ -83,7 +105,9 @@ export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBala
};
export const calculateUsdValue = (data: AddressTokenBalance): TokenEnhancedData => {
if (data.token.type !== 'ERC-20') {
const isFungibleToken = isFungibleTokenType(data.token.type);
if (!isFungibleToken) {
return data;
}
+93 -8
View File
@@ -1,15 +1,20 @@
import { useQueryClient } from '@tanstack/react-query';
import { useQueries, useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressTokenBalance, AddressTokensBalancesSocketMessage, AddressTokensResponse } from 'types/api/address';
import type { TokenType } from 'types/api/token';
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { useMultichainContext } from 'lib/contexts/multichain';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import type { TokenEnhancedData } from './tokenUtils';
import { calculateUsdValue } from './tokenUtils';
interface Props {
hash?: string;
enabled?: boolean;
@@ -21,6 +26,13 @@ const tokenBalanceItemIdentityFactory = (match: AddressTokenBalance) => (item: A
match.token_instance?.id === item.token_instance?.id
));
const socketEventForTokenType = (tokenTypeId: string): string => {
const normalizedTypeId = tokenTypeId.toLowerCase().replace(/[^a-z0-9]+/g, '_');
return `updated_token_balances_${ normalizedTypeId }`;
};
const additionalTokenTypes = config.chain.additionalTokenTypes;
export default function useFetchTokens({ hash, enabled }: Props) {
const erc20query = useApiQuery('general:address_tokens', {
pathParams: { hash },
@@ -43,9 +55,29 @@ export default function useFetchTokens({ hash, enabled }: Props) {
queryOptions: { enabled: Boolean(hash) && enabled, refetchOnMount: false },
});
const apiFetch = useApiFetch();
const multichainContext = useMultichainContext();
const chain = multichainContext?.chain;
const additionalTokenQueries = useQueries({
queries: additionalTokenTypes.map((item) => ({
queryKey: getResourceKey('general:address_tokens', { pathParams: { hash }, queryParams: { type: item.id as unknown as TokenType }, chainId: chain?.id }),
queryFn: async({ signal }) => {
return apiFetch('general:address_tokens', {
pathParams: { hash },
queryParams: { type: item.id as unknown as TokenType },
chain,
fetchParams: { signal },
}) as Promise<AddressTokensResponse>;
},
enabled: Boolean(hash) && enabled,
refetchOnMount: false,
})),
});
const queryClient = useQueryClient();
const updateTokensData = React.useCallback((type: TokenType, payload: AddressTokensBalancesSocketMessage) => {
const updateTokensData = React.useCallback((type: TokenType | Array<TokenType>, payload: AddressTokensBalancesSocketMessage) => {
const queryKey = getResourceKey('general:address_tokens', { pathParams: { hash }, queryParams: { type } });
queryClient.setQueryData(queryKey, (prevData: AddressTokensResponse | undefined) => {
@@ -72,9 +104,15 @@ export default function useFetchTokens({ hash, enabled }: Props) {
});
}, [ hash, queryClient ]);
const additionalTokenTypesIds = React.useMemo(() => {
return additionalTokenTypes.map((item) => item.id);
}, []);
const handleTokenBalancesErc20Message: SocketMessage.AddressTokenBalancesErc20['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-20', payload);
}, [ updateTokensData ]);
// udpate ERC-20 & additional token types query, that is used on the address tokens list
updateTokensData([ 'ERC-20', ...additionalTokenTypesIds ], payload);
}, [ updateTokensData, additionalTokenTypesIds ]);
const handleTokenBalancesErc721Message: SocketMessage.AddressTokenBalancesErc721['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-721', payload);
@@ -84,7 +122,7 @@ export default function useFetchTokens({ hash, enabled }: Props) {
updateTokensData('ERC-1155', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc404Message: SocketMessage.AddressTokenBalancesErc1155['handler'] = React.useCallback((payload) => {
const handleTokenBalancesErc404Message: SocketMessage.AddressTokenBalancesErc404['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-404', payload);
}, [ updateTokensData ]);
@@ -114,7 +152,39 @@ export default function useFetchTokens({ hash, enabled }: Props) {
handler: handleTokenBalancesErc404Message,
});
React.useEffect(() => {
if (!channel || additionalTokenTypes.length === 0) {
return;
}
const refs = additionalTokenTypes.map((item) => {
const event = socketEventForTokenType(item.id);
return channel.on(event, (payload: AddressTokensBalancesSocketMessage) => {
updateTokensData(item.id as unknown as TokenType, payload);
// keep the "ERC-20 & custom token types" list in sync
updateTokensData([ 'ERC-20', ...additionalTokenTypesIds ], payload);
});
});
return () => {
refs.forEach((ref, index) => {
channel.off(socketEventForTokenType(additionalTokenTypes[index]?.id || ''), ref);
});
};
}, [ channel, additionalTokenTypesIds, updateTokensData ]);
const data = React.useMemo(() => {
const additionalGroups = additionalTokenTypes.reduce((result, item, index) => {
const query = additionalTokenQueries[index];
result[item.id] = {
items: query?.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(query?.data?.next_page_params),
};
return result;
}, {} as Record<string, { items: Array<TokenEnhancedData>; isOverflow: boolean }>);
return {
'ERC-20': {
items: erc20query.data?.items.map(calculateUsdValue) || [],
@@ -130,14 +200,29 @@ export default function useFetchTokens({ hash, enabled }: Props) {
},
'ERC-404': {
items: erc404query.data?.items.map(calculateUsdValue) || [],
isOverflow: Boolean(erc1155query.data?.next_page_params),
isOverflow: Boolean(erc404query.data?.next_page_params),
},
...additionalGroups,
};
}, [ erc1155query.data, erc20query.data, erc721query.data, erc404query.data ]);
}, [ additionalTokenQueries, erc1155query.data, erc20query.data, erc721query.data, erc404query.data ]);
const isPending =
erc20query.isPending ||
erc721query.isPending ||
erc1155query.isPending ||
erc404query.isPending ||
additionalTokenQueries.some((query) => query.isPending);
const isError =
erc20query.isError ||
erc721query.isError ||
erc1155query.isError ||
erc404query.isError ||
additionalTokenQueries.some((query) => query.isError);
return {
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending || erc404query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError || erc404query.isError,
isPending,
isError,
data,
};
}
+1 -1
View File
@@ -34,7 +34,7 @@ export const TOKEN = {
total_supply: '76923002799740785',
type: 'ERC-20' as const,
reputation: 'ok' as const,
} satisfies TokenInfo<'ERC-20'>;
} satisfies TokenInfo;
const LinkShowcase = () => {
+18
View File
@@ -21,6 +21,7 @@ import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import useAppActionData from 'ui/shared/AppActionButton/useAppActionData';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import DetailedInfoSponsoredItem from 'ui/shared/DetailedInfo/DetailedInfoSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import AssetValue from 'ui/shared/value/AssetValue';
import TokenNftMarketplaces from './TokenNftMarketplaces';
@@ -91,10 +92,27 @@ const TokenDetails = ({ tokenQuery }: Props) => {
decimals,
symbol,
type,
zilliqa,
} = tokenQuery.data || {};
return (
<DetailedInfo.Container>
{ zilliqa?.zrc2_address_hash && (
<>
<DetailedInfo.ItemLabel
hint="ZRC-2 address of the token"
isLoading={ tokenQuery.isPlaceholderData }
>
ZRC-2 Address
</DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue>
<Skeleton loading={ tokenQuery.isPlaceholderData } display="inline-block">
<AddressEntity address={{ hash: zilliqa.zrc2_address_hash }} isLoading={ tokenQuery.isPlaceholderData }/>
</Skeleton>
</DetailedInfo.ItemValue>
</>
) }
{ exchangeRate && (
<>
<DetailedInfo.ItemLabel
@@ -3,6 +3,7 @@ import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/token';
import { hasTokenIds } from 'lib/token/tokenTypes';
import { TruncatedText } from 'toolkit/components/truncation/TruncatedText';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
@@ -28,7 +29,7 @@ const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => {
/>
</ListItemMobileGrid.Value>
{ (token.type === 'ERC-1155' || token.type === 'ERC-404') && 'token_id' in holder && (
{ (hasTokenIds(token.type)) && 'token_id' in holder && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>ID#</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
+2 -1
View File
@@ -2,6 +2,7 @@ import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/token';
import { hasTokenIds } from 'lib/token/tokenTypes';
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem';
@@ -18,7 +19,7 @@ const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
<TableHeaderSticky top={ top }>
<TableRow>
<TableColumnHeader w="70%">Holder</TableColumnHeader>
{ (token.type === 'ERC-1155' || token.type === 'ERC-404') && <TableColumnHeader w="30%">ID#</TableColumnHeader> }
{ (hasTokenIds(token.type)) && <TableColumnHeader w="30%">ID#</TableColumnHeader> }
<TableColumnHeader isNumeric width="220px">Quantity</TableColumnHeader>
{ token.total_supply && token.type !== 'ERC-404' && <TableColumnHeader isNumeric width="175px">Percentage</TableColumnHeader> }
</TableRow>
@@ -3,6 +3,7 @@ import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/token';
import { hasTokenIds } from 'lib/token/tokenTypes';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import { TruncatedText } from 'toolkit/components/truncation/TruncatedText';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
@@ -26,7 +27,7 @@ const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => {
fontWeight="700"
/>
</TableCell>
{ (token.type === 'ERC-1155' || token.type === 'ERC-404') && 'token_id' in holder && (
{ (hasTokenIds(token.type)) && 'token_id' in holder && (
<TableCell verticalAlign="middle">
<TruncatedText text={ holder.token_id } loading={ isLoading } w="100%"/>
</TableCell>
@@ -5,7 +5,7 @@ import type { TokenInstance } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { ClusterChainConfig } from 'types/multichain';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { hasTokenTransferValue, NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { Badge } from 'toolkit/chakra/badge';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TruncatedText } from 'toolkit/components/truncation/TruncatedText';
@@ -63,7 +63,7 @@ const TokenTransferListItem = ({
w="100%"
fontWeight="500"
/>
{ total && 'value' in total && token && (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
{ total && 'value' in total && token && (hasTokenTransferValue(token.type)) && (
<Flex alignItems="center" columnGap={ 2 } maxW="100%">
<Skeleton
display="inline-flex"
@@ -5,7 +5,7 @@ import type { TokenTransfer } from 'types/api/tokenTransfer';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import { useMultichainContext } from 'lib/contexts/multichain';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { hasTokenIds, hasTokenTransferValue, NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import { TruncatedText } from 'toolkit/components/truncation/TruncatedText';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
@@ -42,9 +42,9 @@ const TokenTransferTable = ({ data, top, showSocketInfo, showSocketErrorAlert, s
<TableColumnHeader width="200px">Method</TableColumnHeader>
<TableColumnHeader width={{ lg: '224px', xl: '380px' }}>From/To</TableColumnHeader>
{ (NFT_TOKEN_TYPE_IDS.includes(tokenType)) &&
<TableColumnHeader width={ tokenType === 'ERC-1155' || tokenType === 'ERC-404' ? '50%' : '100%' }>Token ID</TableColumnHeader>
<TableColumnHeader width={ hasTokenIds(tokenType) ? '50%' : '100%' }>Token ID</TableColumnHeader>
}
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155' || tokenType === 'ERC-404') && (
{ hasTokenTransferValue(tokenType) && (
<TableColumnHeader width={ tokenType === 'ERC-20' ? '100%' : '50%' } isNumeric>
<TruncatedText text={ `Value ${ token?.symbol || '' }` } w="100%" verticalAlign="middle"/>
</TableColumnHeader>
@@ -5,7 +5,7 @@ import type { TokenInstance } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { ClusterChainConfig } from 'types/multichain';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { hasTokenTransferValue, NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { Badge } from 'toolkit/chakra/badge';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table';
@@ -92,7 +92,7 @@ const TokenTransferTableItem = ({
}
</TableCell>
) }
{ token && (token.type === 'ERC-20' || token.type === 'ERC-1155' || token.type === 'ERC-404') && (
{ token && (hasTokenTransferValue(token.type)) && (
<TableCell isNumeric verticalAlign="top">
<AssetValue
amount={ total && 'value' in total ? total.value : null }
+2 -2
View File
@@ -3,7 +3,7 @@ import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { ClusterChainConfig } from 'types/multichain';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { hasTokenTransferValue, NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { Badge } from 'toolkit/chakra/badge';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
@@ -79,7 +79,7 @@ const TokenTransfersListItem = ({ item, isLoading, chainData }: Props) => {
</>
) }
{ item.token && item.total && 'value' in item.total && item.total.value !== null && (item.token.type === 'ERC-20' || item.token.type === 'ERC-1155') && (
{ item.token && item.total && 'value' in item.total && item.total.value !== null && (hasTokenTransferValue(item.token.type)) && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Amount</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
+1 -1
View File
@@ -8,7 +8,7 @@ import GasPriceValue from 'ui/shared/value/GasPriceValue';
import TokenValue from 'ui/shared/value/TokenValue';
interface Props {
gasToken?: TokenInfo<'ERC-20'> | null;
gasToken?: TokenInfo | null;
gasPrice: string | null;
isLoading?: boolean;
}
+5 -18
View File
@@ -15,10 +15,7 @@ import { FormFieldAddress } from 'toolkit/components/forms/fields/FormFieldAddre
import { FormFieldCheckbox } from 'toolkit/components/forms/fields/FormFieldCheckbox';
import { FormFieldText } from 'toolkit/components/forms/fields/FormFieldText';
import AddressFormNotifications from './AddressFormNotifications';
// does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721', 'ERC-404' ] as const;
import AddressFormNotifications, { NOTIFICATIONS } from './AddressFormNotifications';
const TAG_MAX_LENGTH = 35;
@@ -36,19 +33,7 @@ export type Inputs = {
tag: string;
notification: boolean;
notification_settings: {
'native': {
outcoming: boolean;
incoming: boolean;
};
'ERC-721': {
outcoming: boolean;
incoming: boolean;
};
'ERC-20': {
outcoming: boolean;
incoming: boolean;
};
'ERC-404': {
[key: string]: {
outcoming: boolean;
incoming: boolean;
};
@@ -60,7 +45,9 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd,
let notificationsDefault = {} as Inputs['notification_settings'];
if (!data?.notification_settings) {
NOTIFICATIONS.forEach(n => notificationsDefault[n] = { incoming: hasEmail, outcoming: hasEmail });
NOTIFICATIONS.forEach((notificationType) => {
notificationsDefault[notificationType] = { incoming: hasEmail, outcoming: hasEmail };
});
} else {
notificationsDefault = data.notification_settings;
}
@@ -1,5 +1,6 @@
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type { Path } from 'react-hook-form';
import config from 'configs/app';
import { FormFieldCheckbox } from 'toolkit/components/forms/fields/FormFieldCheckbox';
@@ -8,21 +9,29 @@ import type { Inputs as FormFields } from './AddressForm';
const tokenStandardName = config.chain.tokenStandard;
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721', 'ERC-404' ] as const;
const NOTIFICATIONS_NAMES = [
config.chain.currency.symbol,
`${ tokenStandardName }-20`,
`${ tokenStandardName }-721, ${ tokenStandardName }-1155 (NFT)`,
`${ tokenStandardName }-404` ];
interface NotificationOption {
readonly id: string;
readonly label: string;
}
export const NOTIFICATION_OPTIONS: ReadonlyArray<NotificationOption> = [
{ id: 'native', label: config.chain.currency.symbol || '' },
{ id: 'ERC-20', label: `${ tokenStandardName }-20` },
...config.chain.additionalTokenTypes.map((item) => ({ id: item.id, label: item.name })),
{ id: 'ERC-721', label: `${ tokenStandardName }-721, ${ tokenStandardName }-1155 (NFT)` },
{ id: 'ERC-404', label: `${ tokenStandardName }-404` },
];
export const NOTIFICATIONS = NOTIFICATION_OPTIONS.map(({ id }) => id);
export default function AddressFormNotifications() {
return (
<Grid templateColumns={{ base: 'repeat(2, max-content)', lg: 'repeat(3, max-content)' }} gap={{ base: '10px 24px', lg: '20px 24px' }}>
{ NOTIFICATIONS.map((notification, index: number) => {
const incomingFieldName = `notification_settings.${ notification }.incoming` as const;
const outgoingFieldName = `notification_settings.${ notification }.outcoming` as const;
{ NOTIFICATION_OPTIONS.map((notification) => {
const incomingFieldName = `notification_settings.${ notification.id }.incoming` as Path<FormFields>;
const outgoingFieldName = `notification_settings.${ notification.id }.outcoming` as Path<FormFields>;
return (
<React.Fragment key={ notification }>
<React.Fragment key={ notification.id }>
<GridItem
gridColumnStart={{ base: 1, lg: 1 }}
gridColumnEnd={{ base: 3, lg: 1 }}
@@ -30,7 +39,7 @@ export default function AddressFormNotifications() {
mt: { base: 3, lg: 0 },
}}
>
{ NOTIFICATIONS_NAMES[index] }
{ notification.label }
</GridItem>
<GridItem>
<FormFieldCheckbox<FormFields, typeof incomingFieldName>
@@ -41,8 +41,8 @@ export const ZETA_NATIVE_TOKEN = {
icon_url: '',
symbol: config.chain.currency.symbol,
address_hash: 'native',
type: 'ERC-20' as const,
} as TokenInfo<'ERC-20'>;
type: 'ERC-20',
} as TokenInfo;
// We can't implement multivalue search here, because we have different search params for native coin and for tokens.
type Value = TokenInfo | null;
@@ -82,7 +82,7 @@ const ZetaChainFilterByColumn = ({ column, filters, columnName, handleFilterChan
);
}
case 'asset': {
let value: TokenInfo<'ERC-20'> | null = null;
let value: TokenInfo | null = null;
if (filters.coin_type && castArray(filters.coin_type).includes(ZETA_CHAIN_CCTX_COIN_TYPE_FILTER)) {
value = ZETA_NATIVE_TOKEN;
}
@@ -94,7 +94,7 @@ const ZetaChainFilterByColumn = ({ column, filters, columnName, handleFilterChan
decimals: '18',
total_supply: '0',
icon_url: null,
type: 'ERC-20' as const,
type: 'ERC-20',
holders_count: null,
exchange_rate: null,
circulating_market_cap: null,