diff --git a/Dockerfile-dev b/Dockerfile-dev
index dce7f8fdb9..5107e20984 100644
--- a/Dockerfile-dev
+++ b/Dockerfile-dev
@@ -26,6 +26,7 @@ ARG NEXT_PUBLIC_CHAIN_STATUS
ARG NODE_ENV
ARG PORT=3000
+
# Generate env file
ENV NEXT_PUBLIC_GRAPHQL_URL ${NEXT_PUBLIC_GRAPHQL_URL}
ENV NEXT_PUBLIC_GRAPHQL_WS ${NEXT_PUBLIC_GRAPHQL_WS}
@@ -34,6 +35,8 @@ ENV NEXT_PUBLIC_WS_CHAIN_URL ${NEXT_PUBLIC_WS_CHAIN_URL}
ENV NEXT_PUBLIC_CHAIN_STATUS ${NEXT_PUBLIC_CHAIN_STATUS}
ENV NODE_ENV ${NODE_ENV}
ENV PORT ${PORT}
+ENV NODE_OPTIONS="--max-old-space-size=8192"
+
#
# Update schema
diff --git a/Dockerfile-prod b/Dockerfile-prod
index 71479ec7e9..80399e77d2 100644
--- a/Dockerfile-prod
+++ b/Dockerfile-prod
@@ -40,6 +40,7 @@ ENV NEXT_PUBLIC_CHAIN_STATUS ${NEXT_PUBLIC_CHAIN_STATUS}
ENV NEXT_PUBLIC_CHAIN_TYPE ${NEXT_PUBLIC_CHAIN_TYPE}
ENV NODE_ENV ${NODE_ENV}
ENV PORT ${PORT}
+ENV NODE_OPTIONS="--max-old-space-size=8192"
# Update schema
# RUN npm run graphql:codegen
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index b6b43c8054..2660e6d4df 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -1,6 +1,7 @@
version: '3.6'
services:
big-dipper-2:
+ container_name: explorer-v2
build:
context: .
dockerfile: Dockerfile-dev
diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml
index 0df4681305..60ea0885de 100644
--- a/docker-compose-prod.yml
+++ b/docker-compose-prod.yml
@@ -1,6 +1,7 @@
version: '3.6'
services:
big-dipper-2:
+ container_name: explorer-v2
build:
context: .
dockerfile: Dockerfile-prod
diff --git a/i18n.js b/i18n.js
index a31911898a..f41a228a6f 100644
--- a/i18n.js
+++ b/i18n.js
@@ -12,6 +12,7 @@ module.exports = {
'rgx:^/validators': ['validators', 'transactions', 'accounts', 'message_labels', 'message_contents'],
'rgx:^/accounts': ['accounts', 'transactions', 'validators', 'message_labels', 'message_contents'],
'rgx:^/params': ['params'],
+ 'rgx:^/token': ['token', 'transactions'],
},
loadLocaleFrom: (lang, ns) => import(`./public/locales/${lang}/${ns}.json`).then((m) => m.default),
};
diff --git a/public/locales/en/accounts.json b/public/locales/en/accounts.json
index 4fbb28a813..171978abc7 100644
--- a/public/locales/en/accounts.json
+++ b/public/locales/en/accounts.json
@@ -39,5 +39,6 @@
"label": "Label",
"codeId": "Code Id",
"instaBlock": "Instantiated At Block",
- "collateralTransactions": "Collateral Transactions"
+ "collateralTransactions": "Collateral Transactions",
+ "cw20tokens": "CW20 Tokens"
}
diff --git a/public/locales/en/token.json b/public/locales/en/token.json
new file mode 100644
index 0000000000..7ac0257122
--- /dev/null
+++ b/public/locales/en/token.json
@@ -0,0 +1,10 @@
+{
+ "overview": "CW20 Token Details",
+ "name": "Name",
+ "denom": "Denom",
+ "exponent": "Exponent",
+ "circulatingSupply": "Circulating Supply",
+ "minter": "Minter",
+ "maxSupply": "Maximum Supply",
+ "projectUrl": "Project URL"
+}
diff --git a/sample_configs/aura-pool/.env-big-dipper-2 b/sample_configs/aura-pool/.env-big-dipper-2
new file mode 100644
index 0000000000..9cef1849df
--- /dev/null
+++ b/sample_configs/aura-pool/.env-big-dipper-2
@@ -0,0 +1,8 @@
+NEXT_PUBLIC_GRAPHQL_URL=http://34.68.222.131:8080/v1/graphql
+NEXT_PUBLIC_GRAPHQL_WS=wss://34.68.222.131/v1/graphql
+NODE_ENV=production
+PORT=3000
+NEXT_PUBLIC_URL=http://34.68.222.131:3000
+NEXT_PUBLIC_WS_CHAIN_URL=ws://34.121.218.76:26657/websocket
+NEXT_PUBLIC_CHAIN_STATUS=testnet
+LOGGING_DRIVER=gcplogs
\ No newline at end of file
diff --git a/sample_configs/mainnet/.env-big-dipper-2 b/sample_configs/mainnet/.env-big-dipper-2
new file mode 100644
index 0000000000..db8e9b3dd2
--- /dev/null
+++ b/sample_configs/mainnet/.env-big-dipper-2
@@ -0,0 +1,8 @@
+NEXT_PUBLIC_GRAPHQL_URL=https://explorer-gql.cudos.org/v1/graphql
+NEXT_PUBLIC_GRAPHQL_WS=wss://explorer-gql.cudos.org/v1/graphql
+NODE_ENV=production
+PORT=3000
+NEXT_PUBLIC_URL=https://explorer.cudos.org
+NEXT_PUBLIC_WS_CHAIN_URL=wss://mainnet-full-node-01.gcp.service.cudo.org:36657/websocket
+NEXT_PUBLIC_CHAIN_STATUS=mainnet
+LOGGING_DRIVER=gcplogs
\ No newline at end of file
diff --git a/sample_configs/private-testnet/.env-big-dipper-2 b/sample_configs/private-testnet/.env-big-dipper-2
new file mode 100644
index 0000000000..69b043d1f2
--- /dev/null
+++ b/sample_configs/private-testnet/.env-big-dipper-2
@@ -0,0 +1,8 @@
+NEXT_PUBLIC_GRAPHQL_URL=https://explorer-gql.private-testnet.cudos.org/v1/graphql
+NEXT_PUBLIC_GRAPHQL_WS=wss://explorer-gql.private-testnet.cudos.org/v1/graphql
+NODE_ENV=production
+PORT=3000
+NEXT_PUBLIC_URL=https://explorer.private-testnet.cudos.org/
+NEXT_PUBLIC_WS_CHAIN_URL=wss://sentry-01.hosts.private-testnet.cudos.org:36657/websocket
+NEXT_PUBLIC_CHAIN_STATUS=testnet
+LOGGING_DRIVER=gcplogs
\ No newline at end of file
diff --git a/sample_configs/public-testnet/.env-big-dipper-2 b/sample_configs/public-testnet/.env-big-dipper-2
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/components/nav/components/seach_bar/hooks.tsx b/src/components/nav/components/seach_bar/hooks.tsx
index 536f038fb9..1168199dc2 100644
--- a/src/components/nav/components/seach_bar/hooks.tsx
+++ b/src/components/nav/components/seach_bar/hooks.tsx
@@ -7,12 +7,14 @@ import {
BLOCK_DETAILS,
TRANSACTION_DETAILS,
PROFILE_DETAILS,
+ TOKEN_DETAILS,
} from '@utils/go_to_page';
import {
useRecoilCallback,
} from 'recoil';
import { readValidator } from '@recoil/validators';
import { toast } from 'react-toastify';
+import { fetchCW20TokenInfo } from '@src/screens/token_details/utils';
export const useSearchBar = (t) => {
const router = useRouter();
@@ -23,7 +25,7 @@ export const useSearchBar = (t) => {
const validatorRegex = `^(${chainConfig.prefix.validator})`;
const userRegex = `^(${chainConfig.prefix.account})`;
const parsedValue = value.replace(/\s+/g, '');
-
+ const tokenInfo = await fetchCW20TokenInfo(parsedValue);
if (new RegExp(consensusRegex).test(parsedValue)) {
const validatorAddress = await snapshot.getPromise(readValidator(parsedValue));
if (validatorAddress) {
@@ -33,6 +35,8 @@ export const useSearchBar = (t) => {
}
} else if (new RegExp(validatorRegex).test(parsedValue)) {
router.push(VALIDATOR_DETAILS(parsedValue));
+ } else if (tokenInfo.name) {
+ router.push(TOKEN_DETAILS(parsedValue));
} else if (new RegExp(userRegex).test(parsedValue)) {
router.push(ACCOUNT_DETAILS(parsedValue));
} else if (/^@/.test(parsedValue)) {
diff --git a/src/configs/chain_config.mainnet.json b/src/configs/chain_config.mainnet.json
index 2222b6e767..817bfd2637 100644
--- a/src/configs/chain_config.mainnet.json
+++ b/src/configs/chain_config.mainnet.json
@@ -1,6 +1,6 @@
{
"title": "Cudos Block Explorer",
- "network": "cudos-artemis-mainnet",
+ "network": "cudos-1",
"icon": "https://wallet.cudos.org/img/logo-icon.cb505cad.svg?sanitize=true",
"logo": {
"default": "https://wallet.cudos.org/img/logo-dark.e500e625.svg?sanitize=true"
diff --git a/src/configs/chain_config.testnet.json b/src/configs/chain_config.testnet.json
index 4d09b94d7c..341cd4753b 100644
--- a/src/configs/chain_config.testnet.json
+++ b/src/configs/chain_config.testnet.json
@@ -1,6 +1,6 @@
{
"title": "Cudos Block Explorer",
- "network": "cudos-testnet-public-2",
+ "network": "cudos-testnet-public-3",
"icon": "https://wallet.cudos.org/img/logo-icon.cb505cad.svg?sanitize=true",
"logo": {
"default": "https://wallet.cudos.org/img/logo-dark.e500e625.svg?sanitize=true"
diff --git a/src/configs/chain_config_temp.json b/src/configs/chain_config_temp.json
index a48efea98f..7a1d354393 100644
--- a/src/configs/chain_config_temp.json
+++ b/src/configs/chain_config_temp.json
@@ -5,12 +5,12 @@
"links": [
{
"name": "Testnet",
- "chain_id": "cudos-testnet-public-2",
+ "chain_id": "cudos-testnet-public-3",
"url": "https://explorer.testnet.cudos.org/"
},
{
"name": "Mainnet",
- "chain_id": "TBA",
+ "chain_id": "cudos-1",
"url": "https://explorer.cudos.org/"
}
]
diff --git a/src/graphql/cw20_tokens.ts b/src/graphql/cw20_tokens.ts
new file mode 100644
index 0000000000..7a4eee90c1
--- /dev/null
+++ b/src/graphql/cw20_tokens.ts
@@ -0,0 +1,28 @@
+export const Cw20TokenBalancesDocument = /* GraphQL */`
+query Cw20TokenBalances($address: String!) {
+ cw20token_balance(where: {address: {_eq: $address}}) {
+ balance
+ cw20token_info {
+ address
+ name
+ symbol
+ decimals
+ logo
+ }
+ }
+}
+`;
+
+export const Cw20TokenInfoDocument = /* GraphQL */`
+query Cw20TokenInfo($address: String!) {
+ cw20token_info_by_pk(address: $address) {
+ circulating_supply
+ decimals
+ max_supply
+ minter
+ name
+ project_url
+ symbol
+ }
+}
+`;
diff --git a/src/pages/token/[address].tsx b/src/pages/token/[address].tsx
new file mode 100644
index 0000000000..cef6b54975
--- /dev/null
+++ b/src/pages/token/[address].tsx
@@ -0,0 +1,9 @@
+import TokenDetails from '@src/screens/token_details';
+
+const TokenDetailsPage = () => {
+ return (
+
+ );
+};
+
+export default TokenDetailsPage;
diff --git a/src/screens/account_details/components/cw20_token_balances/components/desktop/index.tsx b/src/screens/account_details/components/cw20_token_balances/components/desktop/index.tsx
new file mode 100644
index 0000000000..56fa2e6131
--- /dev/null
+++ b/src/screens/account_details/components/cw20_token_balances/components/desktop/index.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import classnames from 'classnames';
+import useTranslation from 'next-translate/useTranslation';
+import {
+ Table,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableBody,
+} from '@material-ui/core';
+import {
+ AvatarName,
+} from '@components';
+import { formatNumber } from '@utils/format_token';
+import { Cw20TokenBalance } from '@src/screens/account_details/types';
+import { getMiddleEllipsis } from '@utils/get_middle_ellipsis';
+import { TOKEN_DETAILS } from '@src/utils/go_to_page';
+import { columns } from './utils';
+
+const Desktop: React.FC<{
+ className?: string;
+ balances?: Cw20TokenBalance[]
+}> = ({
+ className,
+ balances,
+}) => {
+ const { t } = useTranslation('accounts');
+ const balancesFormatted = balances.map((b) => {
+ const balance = formatNumber(b.balance.toString(), b.exponent);
+ return ({
+ token: (
+
+ ),
+ balance: `${balance} ${b.denom.toUpperCase()}`,
+ address: getMiddleEllipsis(b.tokenAddress, {
+ beginning: 15, ending: 10,
+ }),
+ });
+ });
+
+ return (
+
+
+
+
+ {columns.map((column) => {
+ return (
+
+ {t(column.key)}
+
+ );
+ })}
+
+
+
+ {balancesFormatted.map((b, i) => (
+
+ {columns.map((column) => {
+ return (
+
+ {b[column.key]}
+
+ );
+ })}
+
+ ))}
+
+
+
+ );
+};
+
+export default Desktop;
diff --git a/src/screens/account_details/components/cw20_token_balances/components/desktop/utils.ts b/src/screens/account_details/components/cw20_token_balances/components/desktop/utils.ts
new file mode 100644
index 0000000000..2dcf388bba
--- /dev/null
+++ b/src/screens/account_details/components/cw20_token_balances/components/desktop/utils.ts
@@ -0,0 +1,20 @@
+export const columns:{
+ key: string;
+ align?: 'left' | 'center' | 'right' | 'justify' | 'inherit';
+ width: number;
+}[] = [
+ {
+ key: 'token',
+ width: 40,
+ },
+ {
+ key: 'balance',
+ width: 33,
+ },
+ {
+ key: 'address',
+ width: 33,
+ align: 'right',
+
+ },
+];
diff --git a/src/screens/account_details/components/cw20_token_balances/components/index.ts b/src/screens/account_details/components/cw20_token_balances/components/index.ts
new file mode 100644
index 0000000000..3f1746a986
--- /dev/null
+++ b/src/screens/account_details/components/cw20_token_balances/components/index.ts
@@ -0,0 +1,7 @@
+import Desktop from './desktop';
+import Mobile from './mobile';
+
+export {
+ Desktop,
+ Mobile,
+};
diff --git a/src/screens/account_details/components/cw20_token_balances/components/mobile/index.tsx b/src/screens/account_details/components/cw20_token_balances/components/mobile/index.tsx
new file mode 100644
index 0000000000..8350905910
--- /dev/null
+++ b/src/screens/account_details/components/cw20_token_balances/components/mobile/index.tsx
@@ -0,0 +1,75 @@
+import React from 'react';
+import classnames from 'classnames';
+import useTranslation from 'next-translate/useTranslation';
+import {
+ Divider,
+ Typography,
+} from '@material-ui/core';
+import { AvatarName } from '@components';
+import { formatNumber } from '@utils/format_token';
+import { Cw20TokenBalance } from '@src/screens/account_details/types';
+import { getMiddleEllipsis } from '@src/utils/get_middle_ellipsis';
+import { TOKEN_DETAILS } from '@src/utils/go_to_page';
+import { useStyles } from './styles';
+
+const Mobile: React.FC<{
+ className?: string;
+ balances?: Cw20TokenBalance[]
+}> = ({
+ className,
+ balances,
+}) => {
+ const classes = useStyles();
+ const { t } = useTranslation('accounts');
+
+ return (
+
+ {balances.map((b, i) => {
+ return (
+
+
+
+
+
+
+ {t('balance')}
+
+
+ {formatNumber(b.balance.toString(), b.exponent)}
+ {' '}
+ {b.denom.toUpperCase()}
+
+
+
+
+
+
+ {t('address')}
+
+
+ {getMiddleEllipsis(b.tokenAddress, {
+ beginning: 15, ending: 10,
+ })}
+
+
+
+
+ {i !== balances.length - 1 && }
+
+ );
+ })}
+
+ );
+};
+
+export default Mobile;
diff --git a/src/screens/account_details/components/cw20_token_balances/components/mobile/styles.ts b/src/screens/account_details/components/cw20_token_balances/components/mobile/styles.ts
new file mode 100644
index 0000000000..033ec3b23f
--- /dev/null
+++ b/src/screens/account_details/components/cw20_token_balances/components/mobile/styles.ts
@@ -0,0 +1,51 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = () => {
+ const styles = makeStyles(
+ (theme) => {
+ return ({
+ list: {
+ margin: theme.spacing(2, 0),
+ },
+ item: {
+ marginBottom: theme.spacing(2),
+ '& .label': {
+ marginBottom: theme.spacing(1),
+ color: theme.palette.custom.fonts.fontThree,
+ },
+ '& p.value': {
+ color: theme.palette.custom.fonts.fontTwo,
+ '&.unknown': {
+ color: theme.palette.custom.condition.zero,
+ },
+ '&.unbonded': {
+ color: theme.palette.custom.condition.zero,
+ },
+ '&.active': {
+ color: theme.palette.custom.condition.one,
+ },
+ '&.jailed': {
+ color: theme.palette.custom.condition.two,
+ },
+ '&.unbonding': {
+ color: theme.palette.custom.condition.three,
+ },
+ },
+ '& a': {
+ color: theme.palette.custom.fonts.highlight,
+ },
+ },
+ flex: {
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ '& > div': {
+ width: '50%',
+ },
+ },
+ });
+ },
+ )();
+
+ return styles;
+};
diff --git a/src/screens/account_details/components/cw20_token_balances/index.tsx b/src/screens/account_details/components/cw20_token_balances/index.tsx
new file mode 100644
index 0000000000..436ccfa903
--- /dev/null
+++ b/src/screens/account_details/components/cw20_token_balances/index.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import classnames from 'classnames';
+import dynamic from 'next/dynamic';
+import {
+ usePagination,
+ useScreenSize,
+} from '@hooks';
+import {
+ Pagination,
+ Box,
+} from '@components';
+import { Typography } from '@material-ui/core';
+import useTranslation from 'next-translate/useTranslation';
+import { useStyles } from './styles';
+import { Cw20TokenBalance } from '../../types';
+
+const Desktop = dynamic(() => import('./components/desktop'));
+const Mobile = dynamic(() => import('./components/mobile'));
+
+const Cw20TokenBalances: React.FC<{
+ balances: Cw20TokenBalance[],
+ loading?: boolean
+} & ComponentDefault> = (props) => {
+ if (!props.balances?.length) {
+ return null;
+ }
+
+ const { t } = useTranslation('accounts');
+ const { isDesktop } = useScreenSize();
+ const classes = useStyles();
+ const {
+ page,
+ rowsPerPage,
+ handleChangePage,
+ handleChangeRowsPerPage,
+ sliceItems,
+ } = usePagination({});
+
+ const paginatedBalances = sliceItems(props.balances);
+
+ let component = null;
+ if (isDesktop) {
+ component = ;
+ } else {
+ component = ;
+ }
+
+ return (
+
+
+ {t('cw20tokens')}
+
+
+
+ );
+};
+
+export default Cw20TokenBalances;
diff --git a/src/screens/account_details/components/cw20_token_balances/styles.tsx b/src/screens/account_details/components/cw20_token_balances/styles.tsx
new file mode 100644
index 0000000000..b41ae1afc0
--- /dev/null
+++ b/src/screens/account_details/components/cw20_token_balances/styles.tsx
@@ -0,0 +1,26 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = () => {
+ const styles = makeStyles(
+ (theme) => {
+ return ({
+ paginate: {
+ marginTop: theme.spacing(3),
+ },
+ mobile: {
+ [theme.breakpoints.up('lg')]: {
+ display: 'none',
+ },
+ },
+ desktop: {
+ display: 'none',
+ [theme.breakpoints.up('lg')]: {
+ display: 'flex',
+ },
+ },
+ });
+ },
+ )();
+
+ return styles;
+};
diff --git a/src/screens/account_details/hooks.ts b/src/screens/account_details/hooks.ts
index 0f148b9037..40678e556c 100644
--- a/src/screens/account_details/hooks.ts
+++ b/src/screens/account_details/hooks.ts
@@ -19,6 +19,7 @@ import {
fetchRewards,
fetchUnbondingBalance,
fetchCosmWasmInstantiation,
+ fetchCW20TokenBalances,
} from './utils';
const defaultTokenUnit: TokenUnit = {
@@ -63,6 +64,7 @@ const initialState: AccountDetailState = {
},
},
tab: 0,
+ cw20TokenBalances: [],
};
export const useAccountDetails = () => {
@@ -128,6 +130,7 @@ export const useAccountDetails = () => {
fetchUnbondingBalance(address),
fetchRewards(address),
fetchCosmWasmInstantiation(address),
+ fetchCW20TokenBalances(address),
];
const [
commission,
@@ -136,6 +139,7 @@ export const useAccountDetails = () => {
unbonding,
rewards,
cosmWasmInstantiation,
+ cw20TokenBalances,
] = await Promise.allSettled(promises);
const formattedRawData: any = {};
@@ -144,6 +148,7 @@ export const useAccountDetails = () => {
formattedRawData.delegationBalance = R.pathOr([], ['value', 'delegationBalance'], delegation);
formattedRawData.unbondingBalance = R.pathOr([], ['value', 'unbondingBalance'], unbonding);
formattedRawData.delegationRewards = R.pathOr([], ['value', 'delegationRewards'], rewards);
+ formattedRawData.cw20TokenBalances = R.pathOr([], ['value'], cw20TokenBalances);
handleSetState(formatAllBalance(formattedRawData));
const rawData: any = {};
@@ -299,6 +304,7 @@ export const useAccountDetails = () => {
formatOtherTokens();
stateChange.otherTokens = formatOtherTokens();
+ stateChange.cw20TokenBalances = data.cw20TokenBalances;
return stateChange;
};
diff --git a/src/screens/account_details/index.tsx b/src/screens/account_details/index.tsx
index 7e5843be52..fc8b3b3fe6 100644
--- a/src/screens/account_details/index.tsx
+++ b/src/screens/account_details/index.tsx
@@ -22,6 +22,7 @@ import {
SimpleBalance,
} from './components';
import { useAccountDetails } from './hooks';
+import Cw20TokenBalances from './components/cw20_token_balances';
const AccountDetails = () => {
const { t } = useTranslation('accounts');
@@ -115,6 +116,10 @@ const AccountDetails = () => {
className={classes.otherTokens}
otherTokens={state.otherTokens}
/>
+
{!isSmartContract
&& (
{
// gridColumn: '1 / 3',
},
},
+ cw20TokenBalances: {
+ [theme.breakpoints.up('lg')]: {
+ // gridColumn: '1 / 3',
+ },
+ },
});
},
)();
diff --git a/src/screens/account_details/types.ts b/src/screens/account_details/types.ts
index 76f0aca447..4f19306c28 100644
--- a/src/screens/account_details/types.ts
+++ b/src/screens/account_details/types.ts
@@ -33,6 +33,15 @@ export type CosmwasmType = {
transaction: any
}
+export type Cw20TokenBalance = {
+ tokenAddress: string,
+ name: string
+ denom: string,
+ exponent: number,
+ logo: string,
+ balance: number
+}
+
export type AccountDetailState = {
loading: boolean;
exists: boolean;
@@ -46,4 +55,5 @@ export type AccountDetailState = {
rewards: RewardsType;
cosmwasm: CosmwasmType;
tab: number;
+ cw20TokenBalances: Cw20TokenBalance[];
}
diff --git a/src/screens/account_details/utils.ts b/src/screens/account_details/utils.ts
index 19a31a9276..1154b3dd20 100644
--- a/src/screens/account_details/utils.ts
+++ b/src/screens/account_details/utils.ts
@@ -13,6 +13,9 @@ import {
import {
CosmWasmInstantiateDocument,
} from '@graphql/cosmwasm';
+import {
+ Cw20TokenBalancesDocument,
+} from '@src/graphql/cw20_tokens';
export const fetchCommission = async (address: string) => {
const defaultReturnValue = {
@@ -147,3 +150,40 @@ export const fetchCosmWasmInstantiation = async (address: string) => {
return defaultReturnValue;
}
};
+
+export const fetchCW20TokenBalances = async (address: string) => {
+ const defaultReturnValue = {
+ cw20TokenBalance: [{
+ balance: '0',
+ cw20tokenInfo: {
+ address: '',
+ name: '',
+ logo: '',
+ symbol: '',
+ },
+ }],
+ };
+
+ try {
+ const { data } = await axios.post(process.env.NEXT_PUBLIC_GRAPHQL_URL, {
+ variables: {
+ address,
+ },
+ query: Cw20TokenBalancesDocument,
+ });
+ const balances = R.pathOr(defaultReturnValue, ['data'], data);
+ return balances.cw20token_balance.map((b) => {
+ const info = b.cw20token_info;
+ return {
+ tokenAddress: info.address,
+ name: info.name,
+ denom: info.symbol,
+ exponent: info.decimals,
+ logo: info.logo,
+ balance: b.balance,
+ };
+ });
+ } catch (error) {
+ return defaultReturnValue;
+ }
+};
diff --git a/src/screens/token_details/components/overview/index.tsx b/src/screens/token_details/components/overview/index.tsx
new file mode 100644
index 0000000000..d9ae8f4eaf
--- /dev/null
+++ b/src/screens/token_details/components/overview/index.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import classnames from 'classnames';
+import { BoxDetails } from '@components';
+import useTranslation from 'next-translate/useTranslation';
+import { Cw20TokenInfo } from '../../types';
+
+const Cw20TokenOverview: React.FC<{
+ tokenInfo: Cw20TokenInfo,
+ loading?: boolean
+} & ComponentDefault> = (props) => {
+ if (props.tokenInfo.name === '') {
+ return null;
+ }
+
+ const { t } = useTranslation('token');
+
+ const details = [
+ {
+ label: t('name'),
+ detail: props.tokenInfo.name,
+ },
+ {
+ label: t('denom'),
+ detail: props.tokenInfo.denom,
+ },
+ {
+ label: t('exponent'),
+ detail: props.tokenInfo.exponent,
+ },
+ {
+ label: t('circulatingSupply'),
+ detail: props.tokenInfo.circulatingSupply,
+ },
+ ];
+
+ if (props.tokenInfo.minterAddress !== '') {
+ details.push({
+ label: t('minter'),
+ detail: props.tokenInfo.minterAddress,
+ });
+ }
+
+ if (props.tokenInfo.maxSupply) {
+ details.push({
+ label: t('maxSupply'),
+ detail: props.tokenInfo.maxSupply,
+ });
+ }
+
+ if (props.tokenInfo.projectUrl !== '') {
+ details.push({
+ label: t('projectUrl'),
+ detail: props.tokenInfo.projectUrl,
+ });
+ }
+
+ return (
+
+ );
+};
+
+export default Cw20TokenOverview;
diff --git a/src/screens/token_details/hooks.ts b/src/screens/token_details/hooks.ts
new file mode 100644
index 0000000000..d6d753ebe7
--- /dev/null
+++ b/src/screens/token_details/hooks.ts
@@ -0,0 +1,38 @@
+import {
+ useState, useEffect,
+} from 'react';
+import * as R from 'ramda';
+import { useRouter } from 'next/router';
+import { Cw20TokenInfo } from './types';
+import { fetchCW20TokenInfo } from './utils';
+
+const initialState: Cw20TokenInfo = {
+ address: '',
+ name: '',
+ denom: '',
+ exponent: 0,
+ circulatingSupply: 0,
+};
+
+export const useTokenDetails = () => {
+ const router = useRouter();
+ const [state, setState] = useState(initialState);
+
+ const handleSetState = (stateChange: any) => {
+ setState((prevState) => R.mergeDeepLeft(stateChange, prevState));
+ };
+
+ useEffect(() => {
+ fetchTokenInfo();
+ }, [router.query.address]);
+
+ const fetchTokenInfo = async () => {
+ const address = router.query.address as string;
+ const tokenInfo = await fetchCW20TokenInfo(address);
+ handleSetState(tokenInfo);
+ };
+
+ return {
+ state,
+ };
+};
diff --git a/src/screens/token_details/index.tsx b/src/screens/token_details/index.tsx
new file mode 100644
index 0000000000..263289133c
--- /dev/null
+++ b/src/screens/token_details/index.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import useTranslation from 'next-translate/useTranslation';
+import {
+ Layout,
+ ContractMessages,
+} from '@components';
+import { NextSeo } from 'next-seo';
+import Cw20TokenOverview from './components/overview';
+import { useStyles } from './styles';
+import { useTokenDetails } from './hooks';
+
+const TokenDetails = () => {
+ const { t } = useTranslation('token');
+ const classes = useStyles();
+ const { state } = useTokenDetails();
+ const title = t('cw20TokenDetails');
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default TokenDetails;
diff --git a/src/screens/token_details/styles.ts b/src/screens/token_details/styles.ts
new file mode 100644
index 0000000000..44d21c549d
--- /dev/null
+++ b/src/screens/token_details/styles.ts
@@ -0,0 +1,40 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = () => {
+ const styles = makeStyles(
+ (theme) => {
+ return ({
+ root: {
+ ...theme.mixins.layout,
+ display: 'grid',
+ gridTemplateRows: 'auto',
+ gridGap: theme.spacing(1),
+ '& a': {
+ color: theme.palette.custom.fonts.highlight,
+ },
+ [theme.breakpoints.up('lg')]: {
+ gridGap: theme.spacing(2),
+ // gridTemplateColumns: 'repeat(2, 1fr)',
+ },
+ },
+ overview: {
+ [theme.breakpoints.up('lg')]: {
+ // gridColumn: '1 / 3',
+ },
+ },
+ cw20TokenInfo: {
+ [theme.breakpoints.up('lg')]: {
+ // gridColumn: '1 / 3',
+ },
+ },
+ transactions: {
+ [theme.breakpoints.up('lg')]: {
+ // gridColumn: '1 / 3',
+ },
+ },
+ });
+ },
+ )();
+
+ return styles;
+};
diff --git a/src/screens/token_details/types.ts b/src/screens/token_details/types.ts
new file mode 100644
index 0000000000..10009b11ec
--- /dev/null
+++ b/src/screens/token_details/types.ts
@@ -0,0 +1,10 @@
+export type Cw20TokenInfo = {
+ address: string,
+ name: string,
+ denom: string,
+ exponent: number,
+ circulatingSupply: number,
+ maxSupply?: number,
+ minterAddress?: string,
+ projectUrl?: string,
+}
diff --git a/src/screens/token_details/utils.ts b/src/screens/token_details/utils.ts
new file mode 100644
index 0000000000..bf72fbdac4
--- /dev/null
+++ b/src/screens/token_details/utils.ts
@@ -0,0 +1,43 @@
+import { Cw20TokenInfoDocument } from '@src/graphql/cw20_tokens';
+import axios from 'axios';
+import * as R from 'ramda';
+import { Cw20TokenInfo } from './types';
+
+export const fetchCW20TokenInfo = async (address: string):Promise => {
+ const defaultReturnValue = {
+ cw20TokenInfo: {
+ address: '',
+ name: '',
+ denom: '',
+ logo: '',
+ exponent: 0,
+ circulatingSupply: 0,
+ maxSupply: 0,
+ minterAddress: '',
+ projectUrl: '',
+ },
+ };
+
+ try {
+ const { data } = await axios.post(process.env.NEXT_PUBLIC_GRAPHQL_URL, {
+ variables: {
+ address,
+ },
+ query: Cw20TokenInfoDocument,
+ });
+ const tokenInfo = R.pathOr(defaultReturnValue, ['data', 'cw20token_info_by_pk'], data);
+
+ return {
+ address,
+ name: tokenInfo.name,
+ denom: tokenInfo.symbol,
+ circulatingSupply: tokenInfo.circulating_supply,
+ exponent: tokenInfo.decimals,
+ maxSupply: tokenInfo.max_supply,
+ minterAddress: tokenInfo.minter,
+ projectUrl: tokenInfo.project_url,
+ };
+ } catch (error) {
+ return defaultReturnValue.cw20TokenInfo;
+ }
+};
diff --git a/src/utils/go_to_page.ts b/src/utils/go_to_page.ts
index 52fc56b1ae..568ae8e937 100644
--- a/src/utils/go_to_page.ts
+++ b/src/utils/go_to_page.ts
@@ -10,6 +10,7 @@ export const TRANSACTION_DETAILS = (tx: string): string => `/transactions/${tx}`
export const PROPOSALS = '/proposals';
export const PROPOSAL_DETAILS = (id:string | number): string => `/proposals/${id}`;
export const ACCOUNT_DETAILS = (address: string): string => `/accounts/${address}`;
+export const TOKEN_DETAILS = (address: string): string => `/token/${address}`;
export const PARAMS = '/params';
export const PROFILE_DETAILS = (dtag: string):string => `/${dtag}`;