diff --git a/packages/adena-extension/src/App/use-app.ts b/packages/adena-extension/src/App/use-app.ts index 302de93e..f7257596 100644 --- a/packages/adena-extension/src/App/use-app.ts +++ b/packages/adena-extension/src/App/use-app.ts @@ -26,10 +26,16 @@ const useApp = (): void => { }, [key]); useEffect(() => { - if (currentAccount && currentNetwork) { - initTokenMetainfos(true); + if (!currentAccount?.id) { + return; } - }, [currentAccount, currentNetwork]); + + if (!currentNetwork?.networkId) { + return; + } + + initTokenMetainfos(); + }, [currentAccount?.id, currentNetwork.networkId]); useEffect(() => { initAccountNames(wallet?.accounts ?? []); diff --git a/packages/adena-extension/src/common/provider/wallet/wallet-provider.tsx b/packages/adena-extension/src/common/provider/wallet/wallet-provider.tsx index 407526a7..e7e88594 100644 --- a/packages/adena-extension/src/common/provider/wallet/wallet-provider.tsx +++ b/packages/adena-extension/src/common/provider/wallet/wallet-provider.tsx @@ -1,11 +1,11 @@ +import { Wallet } from 'adena-module'; import React, { createContext, useEffect, useState } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; -import { Wallet } from 'adena-module'; -import { NetworkState, TokenState, WalletState } from '@states'; import { useAdenaContext } from '@hooks/use-context'; +import { NetworkState, TokenState, WalletState } from '@states'; +import { NetworkMetainfo, StateType, TokenModel } from '@types'; import { GnoProvider } from '../gno/gno-provider'; -import { TokenModel, NetworkMetainfo, StateType } from '@types'; export interface WalletContextProps { wallet: Wallet | null; @@ -31,7 +31,7 @@ export const WalletProvider: React.FC> = ({ chi const [walletStatus, setWalletStatus] = useRecoilState(WalletState.state); - const [tokenMetainfos] = useRecoilState(TokenState.tokenMetainfos); + const [tokenMetainfos, setTokenMetainfos] = useRecoilState(TokenState.tokenMetainfos); const [networkMetainfos, setNetworkMetainfos] = useRecoilState(NetworkState.networkMetainfos); @@ -95,6 +95,7 @@ export const WalletProvider: React.FC> = ({ chi if (currentAccount) { setCurrentAccount(currentAccount); await accountService.changeCurrentAccount(currentAccount); + await initTokenMetainfos(currentAccount.id); } return true; } @@ -123,6 +124,13 @@ export const WalletProvider: React.FC> = ({ chi return true; } + async function initTokenMetainfos(accountId: string): Promise { + await tokenService.initAccountTokenMetainfos(accountId); + const tokenMetainfos = await tokenService.getTokenMetainfosByAccountId(accountId); + setTokenMetainfos(tokenMetainfos); + balanceService.setTokenMetainfos(tokenMetainfos); + } + async function changeNetwork(networkMetainfo: NetworkMetainfo): Promise { const rpcUrl = networkMetainfo.rpcUrl; const gnoProvider = new GnoProvider(rpcUrl, networkMetainfo.networkId); diff --git a/packages/adena-extension/src/hooks/use-token-balance.ts b/packages/adena-extension/src/hooks/use-token-balance.ts index 0ffbab75..b0716cb7 100644 --- a/packages/adena-extension/src/hooks/use-token-balance.ts +++ b/packages/adena-extension/src/hooks/use-token-balance.ts @@ -1,16 +1,16 @@ -import { useEffect, useMemo } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Account } from 'adena-module'; +import { useEffect, useMemo } from 'react'; import { isGRC20TokenModel, isNativeTokenModel } from '@common/validation/validation-token'; -import { TokenModel, Amount, TokenBalanceType } from '@types'; +import { Amount, TokenBalanceType, TokenModel } from '@types'; -import { useNetwork } from './use-network'; -import { useCurrentAccount } from './use-current-account'; import { useAdenaContext, useWalletContext } from './use-context'; +import { useCurrentAccount } from './use-current-account'; +import { useGRC20Tokens } from './use-grc20-tokens'; +import { useNetwork } from './use-network'; import { useTokenMetainfo } from './use-token-metainfo'; import { useWallet } from './use-wallet'; -import { useGRC20Tokens } from './use-grc20-tokens'; export const useTokenBalance = (): { mainTokenBalance: Amount | null; @@ -22,6 +22,7 @@ export const useTokenBalance = (): { const { isFetched: isFetchedGRC20Tokens } = useGRC20Tokens(); const { currentTokenMetainfos: tokenMetainfos, + tokenLogoMap, updateTokenMetainfos, getTokenAmount, } = useTokenMetainfo(); @@ -35,12 +36,26 @@ export const useTokenBalance = (): { balanceService.setTokenMetainfos(tokenMetainfos); }, [tokenMetainfos, balanceService]); + const availableBalanceFetching = useMemo(() => { + if (!existWallet || lockedWallet) { + return false; + } + + if (!isFetchedGRC20Tokens || tokenMetainfos.length === 0) { + return false; + } + + return true; + }, [existWallet, lockedWallet, tokenMetainfos, isFetchedGRC20Tokens]); + const { data: balances = [] } = useQuery( [ 'balances', currentAddress, currentNetwork.chainId, tokenMetainfos.map((token) => token.tokenId), + isFetchedGRC20Tokens, + tokenLogoMap, ], () => { if (currentAddress === null || nativeToken == null) { @@ -50,7 +65,10 @@ export const useTokenBalance = (): { tokenMetainfos.map((tokenModel) => fetchBalanceBy(currentAddress, tokenModel)), ); }, - { refetchInterval: 5000, enabled: existWallet && !lockedWallet }, + { + refetchInterval: 5000, + enabled: availableBalanceFetching, + }, ); const { data: accountNativeBalanceMap = {} } = useQuery>( @@ -58,13 +76,14 @@ export const useTokenBalance = (): { 'accountNativeBalanceMap', wallet?.accounts, currentNetwork.chainId, - tokenMetainfos, + tokenMetainfos.map((token) => token.tokenId), isFetchedGRC20Tokens, ], () => { if (wallet === null || wallet.accounts === null || nativeToken == null) { return {}; } + return Promise.all( wallet.accounts.map(async (account) => { const address = await account.getAddress(currentNetwork.addressPrefix); @@ -79,7 +98,10 @@ export const useTokenBalance = (): { }, {}), ); }, - { refetchInterval: 5000, enabled: existWallet && !lockedWallet && isFetchedGRC20Tokens }, + { + refetchInterval: 5000, + enabled: availableBalanceFetching, + }, ); const currentBalances = useMemo((): TokenBalanceType[] => { @@ -133,14 +155,14 @@ export const useTokenBalance = (): { const balanceAmount = isNativeTokenModel(token) ? await balanceService.getGnotTokenBalance(address) : isGRC20TokenModel(token) - ? await balanceService.getGRC20TokenBalance(address, token.pkgPath) + ? await balanceService.getGRC20TokenBalance(address, token.pkgPath, token.decimals) : null; return { ...token, amount: getTokenAmount({ value: `${balanceAmount || 0}`, - denom: isGRC20TokenModel(token) ? token.pkgPath : token.symbol, + denom: token.symbol, }), }; } diff --git a/packages/adena-extension/src/hooks/use-token-metainfo.tsx b/packages/adena-extension/src/hooks/use-token-metainfo.tsx index ca866e4a..268dc7d9 100644 --- a/packages/adena-extension/src/hooks/use-token-metainfo.tsx +++ b/packages/adena-extension/src/hooks/use-token-metainfo.tsx @@ -7,7 +7,7 @@ import { useNetwork } from './use-network'; import { TokenState } from '@states'; import { useQuery } from '@tanstack/react-query'; -import { GRC20TokenModel, TokenModel } from '@types'; +import { GRC20TokenModel, GRC721CollectionModel, TokenModel } from '@types'; import { Account } from 'adena-module'; import BigNumber from 'bignumber.js'; import { useCallback, useMemo } from 'react'; @@ -30,7 +30,7 @@ export type UseTokenMetainfoReturn = { currentTokenMetainfos: TokenModel[]; tokenLogoMap: Record; getTokenAmount: (amount: { value: string; denom: string }) => { value: string; denom: string }; - initTokenMetainfos: (withTransferEvents?: boolean) => Promise; + initTokenMetainfos: () => Promise; updateTokenMetainfos: (account: Account, tokenMetainfos: TokenModel[]) => Promise; addTokenMetainfo: (tokenMetainfo: GRC20TokenModel) => Promise; addGRC20TokenMetainfo: ({ @@ -137,40 +137,51 @@ export const useTokenMetainfo = (): UseTokenMetainfoReturn => { }, {}); }, [allTokenMetainfos]); - const initTokenMetainfos = async (withTransferEvents?: boolean): Promise => { + const initTokenMetainfos = async (): Promise => { if (!currentAccount) { return; } - if (withTransferEvents) { - await setTokenMetainfo([]); - const currentAddress = await currentAccount.getAddress(currentNetwork.addressPrefix); - const transferTokens = await fetchTransferTokens(currentAddress).catch(() => null); - if (transferTokens) { - const storedGRC20Tokens = await tokenService.getTokenMetainfosByAccountId( - currentAccount.id, - ); - const storedCollections = await tokenService.getAccountGRC721Collections( - currentAccount.id, - currentNetwork.chainId, - ); + await setTokenMetainfo([]); + const currentAddress = await currentAccount.getAddress(currentNetwork.addressPrefix); + + /** + * For accounts with no transfer events, initialize the state with the list of stored tokens. + */ + const transferTokens = await fetchTransferTokens(currentAddress).catch(() => null); + if (!transferTokens) { + await tokenService.initAccountTokenMetainfos(currentAccount.id); + const tokenMetainfos = await tokenService.getTokenMetainfosByAccountId(currentAccount.id); + setTokenMetainfo([...tokenMetainfos]); + return; + } - const storedGRC20Packages = storedGRC20Tokens.map((grc20Token) => grc20Token.tokenId); - const storedGRC721Packages = storedCollections.map( - (grc721Token) => grc721Token.packagePath, - ); + /** + * When there is a transfer event, verify that the token is new and not part of the list of tokens stored in the existing account. + * The new tokens are added to the account's token information. + */ + const storedGRC20Tokens = await tokenService.getTokenMetainfosByAccountId(currentAccount.id); + const storedCollections = await tokenService.getAccountGRC721Collections( + currentAccount.id, + currentNetwork.chainId, + ); - const filteredGRC20Packages = (transferTokens.grc20Packages || []).filter( - (grc20Token) => !storedGRC20Packages.includes(grc20Token.tokenId), - ); - const filteredGRC721Packages = (transferTokens.grc721Packages || []).filter( - (grc721Token) => !storedGRC721Packages.includes(grc721Token.packagePath), - ); + const storedGRC20Packages = storedGRC20Tokens + .filter((token: TokenModel) => token.networkId === currentNetwork.networkId) + .map((grc20Token) => grc20Token.tokenId); + const storedGRC721Packages = storedCollections + .filter((token: GRC721CollectionModel) => token.networkId === currentNetwork.networkId) + .map((grc721Token) => grc721Token.packagePath); - await addTokenMetainfos(filteredGRC20Packages); - await addCollections(filteredGRC721Packages); - } - } + const filteredGRC20Packages = (transferTokens.grc20Packages || []).filter( + (grc20Token) => !storedGRC20Packages.includes(grc20Token.tokenId), + ); + const filteredGRC721Packages = (transferTokens.grc721Packages || []).filter( + (grc721Token) => !storedGRC721Packages.includes(grc721Token.packagePath), + ); + + await addTokenMetainfos(filteredGRC20Packages); + await addCollections(filteredGRC721Packages); await tokenService.initAccountTokenMetainfos(currentAccount.id); const tokenMetainfos = await tokenService.getTokenMetainfosByAccountId(currentAccount.id); diff --git a/packages/adena-extension/src/pages/popup/certify/change-network/index.tsx b/packages/adena-extension/src/pages/popup/certify/change-network/index.tsx index a2ba5df7..e1a3127f 100644 --- a/packages/adena-extension/src/pages/popup/certify/change-network/index.tsx +++ b/packages/adena-extension/src/pages/popup/certify/change-network/index.tsx @@ -1,16 +1,14 @@ import React, { useCallback, useEffect, useMemo } from 'react'; +import { CommonFullContentLayout } from '@components/atoms'; import ChangeNetwork from '@components/pages/change-network/change-network/change-network'; +import useAppNavigate from '@hooks/use-app-navigate'; import { useNetwork } from '@hooks/use-network'; -import { useTokenMetainfo } from '@hooks/use-token-metainfo'; import { RoutePath } from '@types'; -import { CommonFullContentLayout } from '@components/atoms'; -import useAppNavigate from '@hooks/use-app-navigate'; const ChangeNetworkContainer: React.FC = () => { const { navigate, goBack } = useAppNavigate(); const { modified, currentNetwork, networks, setModified, changeNetwork } = useNetwork(); - const { initTokenMetainfos } = useTokenMetainfo(); useEffect(() => { if (modified) { @@ -46,7 +44,6 @@ const ChangeNetworkContainer: React.FC = () => { if (networkId) { await changeNetwork(networkId); - await initTokenMetainfos(); navigate(RoutePath.Wallet); } }; diff --git a/packages/adena-extension/src/pages/popup/wallet/manage-token-added/index.tsx b/packages/adena-extension/src/pages/popup/wallet/manage-token-added/index.tsx index 32d62f06..a2874069 100644 --- a/packages/adena-extension/src/pages/popup/wallet/manage-token-added/index.tsx +++ b/packages/adena-extension/src/pages/popup/wallet/manage-token-added/index.tsx @@ -75,8 +75,11 @@ const ManageTokenAddedContainer: React.FC = () => { } const isRegistered = tokenMetainfos.some((tokenMetaInfo) => { - if (tokenMetaInfo.tokenId === manualTokenPath) { - return true; + if ( + tokenMetaInfo.tokenId !== manualTokenPath || + tokenMetaInfo.networkId !== currentNetwork.networkId + ) { + return false; } if (isGRC20TokenModel(tokenMetaInfo)) { diff --git a/packages/adena-extension/src/repositories/common/token.queries.ts b/packages/adena-extension/src/repositories/common/token.queries.ts index 845109e0..a3481b9e 100644 --- a/packages/adena-extension/src/repositories/common/token.queries.ts +++ b/packages/adena-extension/src/repositories/common/token.queries.ts @@ -26,6 +26,52 @@ export const makeAllRealmsQuery = (): string => ` `; export const makeGRC721TransferEventsQuery = (packagePath: string, address: string): string => ` +{ + transactions( + filter: { + success: true + events: { + type: "Transfer" + pkg_path: "${packagePath}" + attrs: [{ + key: "from" + value: "${address}" + }, { + key:"to" + value: "${address}" + }] + } + messages: [ + { + type_url: exec + } + ] + } + ) { + hash + index + success + block_height + response { + events { + ...on GnoEvent { + type + pkg_path + func + attrs { + key + value + } + } + } + } + } +} +`; +export const makeGRC721TransferEventsQueryWithCursor = ( + packagePath: string, + address: string, +): string => ` { transactions( filter: { @@ -81,6 +127,40 @@ export const makeGRC721TransferEventsQuery = (packagePath: string, address: stri `; export const makeAllTransferEventsQueryBy = (address: string): string => ` +{ + transactions( + filter: { + success: true + events: { + type: "Transfer" + attrs: [{ + key: "to" + value: "${address}" + }] + } + } + ) { + hash + index + success + block_height + response { + events { + ...on GnoEvent { + type + pkg_path + func + attrs { + key + value + } + } + } + } + } +}`; + +export const makeAllTransferEventsQueryWithCursorBy = (address: string): string => ` { transactions( filter: { diff --git a/packages/adena-extension/src/repositories/common/token.ts b/packages/adena-extension/src/repositories/common/token.ts index ae6e857f..f59fd7e6 100644 --- a/packages/adena-extension/src/repositories/common/token.ts +++ b/packages/adena-extension/src/repositories/common/token.ts @@ -35,6 +35,7 @@ import { makeAllRealmsQuery, makeAllTransferEventsQueryBy, makeGRC721TransferEventsQuery, + makeGRC721TransferEventsQueryWithCursor, } from './token.queries'; import { ITokenRepository } from './types'; @@ -268,6 +269,7 @@ export class TokenRepository implements ITokenRepository { .map((message: any) => mapGRC20TokenModel(this.networkMetainfo?.networkId || '', message), ) + .filter((tokenInfo: GRC20TokenModel | null) => !!tokenInfo) : [], ); }; @@ -323,15 +325,12 @@ export class TokenRepository implements ITokenRepository { .map((message: any) => mapGRC721CollectionModel(this.networkMetainfo?.networkId || '', message), ) + .filter((collection: GRC721CollectionModel | null) => !!collection) : [], ); } public async fetchAllTransferPackagesBy(address: string): Promise { - if (!this.apiUrl || !this.queryUrl) { - return []; - } - if (this.apiUrl) { const packages = await TokenRepository.postRPCRequest<{ result: string[]; @@ -349,19 +348,23 @@ export class TokenRepository implements ITokenRepository { return packages; } + if (!this.queryUrl) { + return []; + } + const transferEventsQuery = makeAllTransferEventsQueryBy(address); return TokenRepository.postGraphQuery( this.networkInstance, this.queryUrl, transferEventsQuery, ).then((result) => { - const edges = result?.data?.transactions?.edges; - if (!edges) { + const transactions = result?.data?.transactions; + if (!transactions) { return []; } - const packagePaths: string[] = edges - .flatMap((edge: any) => edge?.transaction?.response?.events || []) + const packagePaths: string[] = transactions + .flatMap((transaction: any) => transaction?.response?.events || []) .filter((event: any) => { const eventType = event?.type; const eventAttributes = event?.attrs || []; @@ -460,25 +463,59 @@ export class TokenRepository implements ITokenRepository { } public async fetchGRC721TokensBy(packagePath: string, address: string): Promise { - if (!this.queryUrl) { + if (!this.apiUrl && !this.queryUrl) { return []; } - const grc721TransferEventsQuery = makeGRC721TransferEventsQuery(packagePath, address); const events: { type: string; pkg_path: string; func: string; attrs: { [key in string]: string }[]; - }[] = await TokenRepository.postGraphQuery( - this.networkInstance, - this.queryUrl, - grc721TransferEventsQuery, - ).then((result) => - result?.data?.transactions - ? result?.data?.transactions?.edges.flatMap((edge: any) => edge.transaction.response.events) - : [], - ); + }[] = []; + + if (this.apiUrl) { + const grc721TransferEventsQuery = makeGRC721TransferEventsQueryWithCursor( + packagePath, + address, + ); + const resultEvents: { + type: string; + pkg_path: string; + func: string; + attrs: { [key in string]: string }[]; + }[] = await TokenRepository.postGraphQuery( + this.networkInstance, + this.queryUrl || this.apiUrl, + grc721TransferEventsQuery, + ).then((result) => + result?.data?.transactions + ? result?.data?.transactions?.edges.flatMap( + (edge: any) => edge.transaction.response.events, + ) + : [], + ); + + events.push(...resultEvents); + } else { + const grc721TransferEventsQuery = makeGRC721TransferEventsQuery(packagePath, address); + const resultEvents: { + type: string; + pkg_path: string; + func: string; + attrs: { [key in string]: string }[]; + }[] = await TokenRepository.postGraphQuery( + this.networkInstance, + this.queryUrl || '', + grc721TransferEventsQuery, + ).then((result) => + result?.data?.transactions + ? result?.data?.transactions?.flatMap((transaction: any) => transaction?.response?.events) + : [], + ); + + events.push(...resultEvents); + } const receivedTokenIds: string[] = []; const sendedTokenIds: string[] = []; diff --git a/packages/adena-extension/src/services/wallet/wallet-balance.ts b/packages/adena-extension/src/services/wallet/wallet-balance.ts index c7ad2078..f0816f76 100644 --- a/packages/adena-extension/src/services/wallet/wallet-balance.ts +++ b/packages/adena-extension/src/services/wallet/wallet-balance.ts @@ -48,7 +48,11 @@ export class WalletBalanceService { .catch(() => null); } - public async getGRC20TokenBalance(address: string, packagePath: string): Promise { + public async getGRC20TokenBalance( + address: string, + packagePath: string, + decimals = 6, + ): Promise { const gnoProvider = this.getGnoProvider(); return gnoProvider .getValueByEvaluateExpression(packagePath, 'BalanceOf', [address]) @@ -56,7 +60,9 @@ export class WalletBalanceService { if (result === null || !BigNumber(result).isInteger()) { return null; } - return BigNumber(result).toNumber(); + return BigNumber(result) + .shiftedBy(decimals * -1) + .toNumber(); }) .catch(() => null); }