diff --git a/packages/adena-extension/src/common/constants/interval.constant.ts b/packages/adena-extension/src/common/constants/interval.constant.ts new file mode 100644 index 000000000..1add9e141 --- /dev/null +++ b/packages/adena-extension/src/common/constants/interval.constant.ts @@ -0,0 +1 @@ +export const HISTORY_FETCH_INTERVAL_TIME = 3 * 1000; 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 0c70c0552..c3b2f087d 100644 --- a/packages/adena-extension/src/common/provider/wallet/wallet-provider.tsx +++ b/packages/adena-extension/src/common/provider/wallet/wallet-provider.tsx @@ -22,7 +22,7 @@ interface WalletContextProps { export const WalletContext = createContext(null); export const WalletProvider: React.FC> = ({ children }) => { - const { walletService, transactionService, balanceService, accountService, chainService, tokenService } = + const { walletService, transactionService, balanceService, accountService, chainService, tokenService, transactionHistoryService } = useAdenaContext(); const [gnoProvider, setGnoProvider] = useState(); @@ -136,6 +136,8 @@ export const WalletProvider: React.FC> = ({ chi accountService.setGnoProvider(gnoProvider); balanceService.setGnoProvider(gnoProvider); transactionService.setGnoProvider(gnoProvider); + tokenService.setNetworkMetainfo(networkMetainfo); + transactionHistoryService.setNetworkMetainfo(networkMetainfo); return networkMetainfo; } diff --git a/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.spec.tsx b/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.spec.tsx index dc07f0c55..bc8ff24bc 100644 --- a/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.spec.tsx +++ b/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.spec.tsx @@ -13,6 +13,7 @@ describe('TransferInput Component', () => { main: true, display: false, tokenId: 'Gnoland', + networkId: 'DEFAULT', name: 'Gnoland', image: '', symbol: 'GNOT', diff --git a/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.stories.tsx b/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.stories.tsx index 6150a476d..fd0b5f8e3 100644 --- a/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.stories.tsx +++ b/packages/adena-extension/src/components/transfer/transfer-input/transfer-input.stories.tsx @@ -13,6 +13,7 @@ export const Default: StoryObj = { main: true, display: false, tokenId: 'Gnoland', + networkId: 'DEFAULT', name: 'Gnoland', image: '', symbol: 'GNOT', diff --git a/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.spec.tsx b/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.spec.tsx index 0a816d62f..9c1e0a98f 100644 --- a/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.spec.tsx +++ b/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.spec.tsx @@ -12,6 +12,7 @@ describe('TransferSummary Component', () => { tokenMetainfo: { main: true, tokenId: 'Gnoland', + networkId: 'DEFAULT', name: 'Gnoland', image: '', symbol: 'GNOT', diff --git a/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.stories.tsx b/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.stories.tsx index d93c80163..cc5cbc5ad 100644 --- a/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.stories.tsx +++ b/packages/adena-extension/src/components/transfer/transfer-summary/transfer-summary.stories.tsx @@ -14,6 +14,7 @@ export const Default: StoryObj = { main: true, display: false, tokenId: 'Gnoland', + networkId: 'DEFAULT', name: 'Gnoland', image: '', symbol: 'GNOT', diff --git a/packages/adena-extension/src/containers/history-container/history-container.tsx b/packages/adena-extension/src/containers/history-container/history-container.tsx index c0ebe959d..a491bce80 100644 --- a/packages/adena-extension/src/containers/history-container/history-container.tsx +++ b/packages/adena-extension/src/containers/history-container/history-container.tsx @@ -11,6 +11,7 @@ import UnknownTokenIcon from '@assets/common-unknown-token.svg'; import { useNetwork } from '@hooks/use-network'; import useScrollHistory from '@hooks/use-scroll-history'; import BigNumber from 'bignumber.js'; +import { HISTORY_FETCH_INTERVAL_TIME } from '@common/constants/interval.constant'; const HistoryContainer: React.FC = () => { const navigate = useNavigate(); @@ -43,8 +44,8 @@ const HistoryContainer: React.FC = () => { useEffect(() => { if (currentAddress) { const historyFetchTimer = setInterval(() => { - refetch({ refetchPage: (page, index) => index === 0 }) - }, 10 * 1000); + refetch({ refetchPage: (_, index) => index === 0 }) + }, HISTORY_FETCH_INTERVAL_TIME); return () => clearInterval(historyFetchTimer); } }, [currentAddress, refetch]); @@ -76,7 +77,7 @@ const HistoryContainer: React.FC = () => { }; const fetchTokenHistories = async (pageParam: number) => { - if (!currentAddress || currentNetwork.networkId !== 'test3') { + if (!currentAddress) { return { hits: 0, next: false, diff --git a/packages/adena-extension/src/containers/manage-token-search-container/manage-token-search-container.tsx b/packages/adena-extension/src/containers/manage-token-search-container/manage-token-search-container.tsx index 4b3d2f404..45abe7fc4 100644 --- a/packages/adena-extension/src/containers/manage-token-search-container/manage-token-search-container.tsx +++ b/packages/adena-extension/src/containers/manage-token-search-container/manage-token-search-container.tsx @@ -28,7 +28,7 @@ const ManageTokenSearchContainer: React.FC = () => { const [isClose, setIsClose] = useState(false); const { tokenMetainfos } = useTokenMetainfo(); const { currentAccount } = useCurrentAccount(); - const { tokenBalances, toggleDisplayOption, updateTokenBalanceInfos } = useTokenBalance(); + const { displayTokenBalances: tokenBalances, toggleDisplayOption, updateTokenBalanceInfos } = useTokenBalance(); useEffect(() => { if (currentAccount) { diff --git a/packages/adena-extension/src/containers/token-details-container/token-details-container.tsx b/packages/adena-extension/src/containers/token-details-container/token-details-container.tsx index 98c40a967..ec2a76a2c 100644 --- a/packages/adena-extension/src/containers/token-details-container/token-details-container.tsx +++ b/packages/adena-extension/src/containers/token-details-container/token-details-container.tsx @@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom'; import { RoutePath } from '@router/path'; import { TransactionHistoryMapper } from '@repositories/transaction/mapper/transaction-history-mapper'; import UnknownTokenIcon from '@assets/common-unknown-token.svg'; +import { HISTORY_FETCH_INTERVAL_TIME } from '@common/constants/interval.constant'; const TokenDetailsContainer: React.FC = () => { const navigate = useNavigate(); @@ -38,8 +39,8 @@ const TokenDetailsContainer: React.FC = () => { useEffect(() => { if (currentAddress) { const historyFetchTimer = setInterval(() => { - refetch({ refetchPage: (page, index) => index === 0 }) - }, 10 * 1000); + refetch({ refetchPage: (_, index) => index === 0 }) + }, HISTORY_FETCH_INTERVAL_TIME); return () => clearInterval(historyFetchTimer); } }, [currentAddress, refetch]); diff --git a/packages/adena-extension/src/hooks/use-network.ts b/packages/adena-extension/src/hooks/use-network.ts index 1d3660277..140ec0e52 100644 --- a/packages/adena-extension/src/hooks/use-network.ts +++ b/packages/adena-extension/src/hooks/use-network.ts @@ -5,6 +5,7 @@ import { NetworkState } from '@states/index'; import { EventMessage } from '@inject/message'; import { useCallback } from 'react'; import { useEvent } from './use-event'; +import { useTokenMetainfo } from './use-token-metainfo'; interface NetworkResponse { networks: NetworkMetainfo[]; @@ -16,6 +17,7 @@ interface NetworkResponse { const DEFAULT_NETWORK: NetworkMetainfo = { id: 'test3', default: true, + main: true, chainId: 'GNOLAND', chainName: 'GNO.LAND', networkId: 'test3', @@ -25,12 +27,6 @@ const DEFAULT_NETWORK: NetworkMetainfo = { gnoUrl: 'https://test3.gno.land', apiUrl: 'https://api.adena.app', linkUrl: 'https://gnoscan.io', - token: { - denom: 'gnot', - unit: 1, - minimalDenom: 'ugnot', - minimalUnit: 0.000001, - }, }; export const useNetwork = (): NetworkResponse => { diff --git a/packages/adena-extension/src/hooks/use-token-balance.ts b/packages/adena-extension/src/hooks/use-token-balance.ts index 472be453e..e3bf00449 100644 --- a/packages/adena-extension/src/hooks/use-token-balance.ts +++ b/packages/adena-extension/src/hooks/use-token-balance.ts @@ -63,8 +63,10 @@ export const useTokenBalance = (): { }, [getTokenBalancesByAccount, currentAccount]); const getDisplayTokenBalances = useCallback(() => { - return getCurrentTokenBalances().filter((token) => token.display); - }, [getTokenBalancesByAccount, currentAccount]); + return getCurrentTokenBalances().filter( + (token) => token.networkId === currentNetwork.networkId || token.networkId === 'DEFAULT', + ); + }, [getTokenBalancesByAccount, currentAccount?.id, currentNetwork.networkId]); function matchNetworkId(accountTokenBalance: AccountTokenBalance) { return accountTokenBalance.networkId === currentNetwork?.id; @@ -72,7 +74,11 @@ export const useTokenBalance = (): { function matchCurrentAccount(account: Account | null, accountTokenBalance: AccountTokenBalance) { if (!account) return false; - return accountTokenBalance.accountId === account.id && matchNetworkId(accountTokenBalance); + return ( + (accountTokenBalance.accountId === account.id && matchNetworkId(accountTokenBalance)) || + accountTokenBalance.networkId === 'DEFAULT' || + accountTokenBalance.networkId === currentNetwork.networkId + ); } async function toggleDisplayOption(account: Account, token: TokenModel, activated: boolean) { @@ -94,6 +100,7 @@ export const useTokenBalance = (): { return accountTokenBalance; }); setAccountTokenBalances(changedAccountTokenBalances); + console.log(changedAccountTokenBalances); await tokenService.updateAccountTokenMetainfos(changedAccountTokenBalances); } diff --git a/packages/adena-extension/src/hooks/use-token-metainfo.tsx b/packages/adena-extension/src/hooks/use-token-metainfo.tsx index 41e0d7050..ea7875a42 100644 --- a/packages/adena-extension/src/hooks/use-token-metainfo.tsx +++ b/packages/adena-extension/src/hooks/use-token-metainfo.tsx @@ -3,6 +3,7 @@ import { useRecoilState } from "recoil"; import { useAdenaContext } from "./use-context"; import { useCurrentAccount } from "./use-current-account"; import { GRC20TokenModel, TokenModel, isGRC20TokenModel, isNativeTokenModel } from "@models/token-model"; +import { useNetwork } from "./use-network"; interface GRC20Token { tokenId: string; @@ -17,6 +18,7 @@ export const useTokenMetainfo = () => { const { balanceService, tokenService } = useAdenaContext(); const [tokenMetainfos, setTokenMetainfo] = useRecoilState(TokenState.tokenMetainfos); const { currentAccount } = useCurrentAccount(); + const { currentNetwork } = useNetwork(); const initTokenMetainfos = async () => { if (currentAccount) { @@ -96,6 +98,7 @@ export const useTokenMetainfo = () => { const tokenMetainfo: GRC20TokenModel = { main: false, tokenId, + networkId: currentNetwork.networkId, pkgPath: path, symbol, type: 'grc20', diff --git a/packages/adena-extension/src/inject/message/methods/core.ts b/packages/adena-extension/src/inject/message/methods/core.ts index 37e54e86f..384b8b19d 100644 --- a/packages/adena-extension/src/inject/message/methods/core.ts +++ b/packages/adena-extension/src/inject/message/methods/core.ts @@ -56,6 +56,7 @@ export class InjectCore { public async initGnoProvider() { try { const network = await this.chainService.getCurrentNetwork(); + this.tokenService.setNetworkMetainfo(network); this.gnoProvider = new GnoProvider(network.rpcUrl, network.networkId); this.accountService.setGnoProvider(this.gnoProvider); this.transactionService.setGnoProvider(this.gnoProvider); diff --git a/packages/adena-extension/src/migrates/migrations/v004/storage-migration-v004.spec.ts b/packages/adena-extension/src/migrates/migrations/v004/storage-migration-v004.spec.ts new file mode 100644 index 000000000..529ddc461 --- /dev/null +++ b/packages/adena-extension/src/migrates/migrations/v004/storage-migration-v004.spec.ts @@ -0,0 +1,86 @@ +import { decryptAES } from 'adena-module'; +import { StorageMigration004 } from './storage-migration-v004'; + +const mockStorageData = { + NETWORKS: [], + CURRENT_CHAIN_ID: '', + CURRENT_NETWORK_ID: '', + SERIALIZED: 'U2FsdGVkX19eI8kOCI/T9o1Ru0b2wdj5rHxmG4QbLQ0yZH4kDa8/gg6Ac2JslvEm', + ENCRYPTED_STORED_PASSWORD: '', + CURRENT_ACCOUNT_ID: '', + ACCOUNT_NAMES: {}, + ESTABLISH_SITES: {}, + ADDRESS_BOOK: [], + ACCOUNT_TOKEN_METAINFOS: {}, +}; + +describe('serialized wallet migration V003', () => { + it('version', () => { + const migration = new StorageMigration004(); + expect(migration.version).toBe(4); + }); + + it('up success', async () => { + const mockData = { + version: 2, + data: mockStorageData, + }; + const migration = new StorageMigration004(); + const result = await migration.up(mockData); + + expect(result.version).toBe(4); + expect(result.data).not.toBeNull(); + expect(result.data.NETWORKS).toEqual([]); + expect(result.data.CURRENT_CHAIN_ID).toBe(''); + expect(result.data.CURRENT_NETWORK_ID).toBe(''); + expect(result.data.SERIALIZED).toBe( + 'U2FsdGVkX19eI8kOCI/T9o1Ru0b2wdj5rHxmG4QbLQ0yZH4kDa8/gg6Ac2JslvEm', + ); + expect(result.data.ENCRYPTED_STORED_PASSWORD).toBe(''); + expect(result.data.CURRENT_ACCOUNT_ID).toBe(''); + expect(result.data.ACCOUNT_NAMES).toEqual({}); + expect(result.data.ESTABLISH_SITES).toEqual({}); + expect(result.data.ADDRESS_BOOK).toEqual([]); + }); + + it('up password success', async () => { + const mockData = { + version: 1, + data: mockStorageData, + }; + const password = '123'; + const migration = new StorageMigration004(); + const result = await migration.up(mockData, password); + + expect(result.version).toBe(4); + expect(result.data).not.toBeNull(); + expect(result.data.NETWORKS).toEqual([]); + expect(result.data.CURRENT_CHAIN_ID).toBe(''); + expect(result.data.CURRENT_NETWORK_ID).toBe(''); + expect(result.data.SERIALIZED).not.toBe(''); + expect(result.data.ENCRYPTED_STORED_PASSWORD).toBe(''); + expect(result.data.CURRENT_ACCOUNT_ID).toBe(''); + expect(result.data.ACCOUNT_NAMES).toEqual({}); + expect(result.data.ESTABLISH_SITES).toEqual({}); + expect(result.data.ADDRESS_BOOK).toEqual([]); + + const serialized = result.data.SERIALIZED; + const decrypted = await decryptAES(serialized, password); + const wallet = JSON.parse(decrypted); + + expect(wallet.accounts).toHaveLength(0); + expect(wallet.keyrings).toHaveLength(0); + }); + + it('up failed throw error', async () => { + const mockData: any = { + version: 1, + data: { ...mockStorageData, SERIALIZED: null }, + }; + const migration = new StorageMigration004(); + + await expect(migration.up(mockData)).rejects.toThrow( + 'Stroage Data doesn not match version V003', + ); + }); +}); diff --git a/packages/adena-extension/src/migrates/migrations/v004/storage-migration-v004.ts b/packages/adena-extension/src/migrates/migrations/v004/storage-migration-v004.ts new file mode 100644 index 000000000..1f6b70872 --- /dev/null +++ b/packages/adena-extension/src/migrates/migrations/v004/storage-migration-v004.ts @@ -0,0 +1,118 @@ +import { Migration } from '@migrates/migrator'; +import { StorageModel } from '@common/storage'; +import { + AccountTokenMetainfoModelV003, + NetworksModelV003, + StorageModelDataV003, +} from '../v003/storage-model-v003'; +import { + AccountTokenMetainfoModelV004, + NetworksModelV004, + StorageModelDataV004, +} from './storage-model-v004'; + +export class StorageMigration004 implements Migration { + public readonly version = 4; + + async up( + current: StorageModel, + password?: string, + ): Promise> { + if (!this.validateModelV003(current.data)) { + throw new Error('Stroage Data doesn not match version V003'); + } + const previous: StorageModelDataV003 = current.data; + return { + version: this.version, + data: { + ...previous, + NETWORKS: this.migrateNetworks(previous.NETWORKS), + ACCOUNT_TOKEN_METAINFOS: this.migrateAccountTokenMetainfo(previous.ACCOUNT_TOKEN_METAINFOS), + }, + }; + } + + private validateModelV003(currentData: StorageModelDataV003) { + const storageDataKeys = [ + 'NETWORKS', + 'CURRENT_CHAIN_ID', + 'CURRENT_NETWORK_ID', + 'SERIALIZED', + 'ENCRYPTED_STORED_PASSWORD', + 'CURRENT_ACCOUNT_ID', + 'ACCOUNT_NAMES', + 'ESTABLISH_SITES', + 'ADDRESS_BOOK', + 'ACCOUNT_TOKEN_METAINFOS', + ]; + const hasKeys = Object.keys(currentData).every((dataKey) => storageDataKeys.includes(dataKey)); + if (!hasKeys) { + return false; + } + if (!Array.isArray(currentData.NETWORKS)) { + return false; + } + if (typeof currentData.CURRENT_CHAIN_ID !== 'string') { + return false; + } + if (typeof currentData.CURRENT_NETWORK_ID !== 'string') { + return false; + } + if (typeof currentData.SERIALIZED !== 'string') { + return false; + } + if (typeof currentData.ENCRYPTED_STORED_PASSWORD !== 'string') { + return false; + } + if (typeof currentData.CURRENT_ACCOUNT_ID !== 'string') { + return false; + } + if (typeof currentData.ACCOUNT_NAMES !== 'object') { + return false; + } + if (typeof currentData.ESTABLISH_SITES !== 'object') { + return false; + } + if (typeof currentData.ADDRESS_BOOK !== 'object') { + return false; + } + return true; + } + + private migrateNetworks(networksDataV003: NetworksModelV003): NetworksModelV004 { + const networks = networksDataV003.map((network: any, index) => { + if (index < 3) { + return { + ...network, + id: network.networkId, + default: true, + main: network.networkId === 'test3', + }; + } + return { + ...network, + id: network.networkId || Date.now(), + default: false, + main: false, + }; + }); + return networks; + } + + private migrateAccountTokenMetainfo( + accountTokenMetainfo: AccountTokenMetainfoModelV003, + ): AccountTokenMetainfoModelV004 { + const changedAccountTokenMetainfo: AccountTokenMetainfoModelV004 = {}; + for (const accountId of Object.keys(accountTokenMetainfo)) { + const tokenMetainfos = accountTokenMetainfo[accountId].map((tokenMetainfo) => ({ + ...tokenMetainfo, + networkId: + tokenMetainfo.type === 'gno-native' && tokenMetainfo.symbol === 'GNOT' + ? 'DEFAULT' + : 'test3', + })); + changedAccountTokenMetainfo[accountId] = tokenMetainfos; + } + return changedAccountTokenMetainfo; + } +} diff --git a/packages/adena-extension/src/migrates/migrations/v004/storage-model-v004.ts b/packages/adena-extension/src/migrates/migrations/v004/storage-model-v004.ts new file mode 100644 index 000000000..277b268f2 --- /dev/null +++ b/packages/adena-extension/src/migrates/migrations/v004/storage-model-v004.ts @@ -0,0 +1,117 @@ +export type StorageModelV004 = { + version: 2; + data: StorageModelDataV004; +}; + +export type StorageModelDataV004 = { + NETWORKS: NetworksModelV004; + CURRENT_CHAIN_ID: CurrentChainIdModelV004; + CURRENT_NETWORK_ID: CurrentNetworkIdModelV004; + SERIALIZED: SerializedModelV004; + ENCRYPTED_STORED_PASSWORD: EncryptedStoredPasswordModelV004; + CURRENT_ACCOUNT_ID: CurrentAccountIdModelV004; + ACCOUNT_NAMES: AccountNamesModelV004; + ESTABLISH_SITES: EstablishSitesModelV004; + ADDRESS_BOOK: AddressBookModelV004; + ACCOUNT_TOKEN_METAINFOS: AccountTokenMetainfoModelV004; +}; + +export type NetworksModelV004 = { + id: string; + default: boolean; + main: boolean; + chainId: string; + chainName: string; + networkId: string; + networkName: string; + addressPrefix: string; + rpcUrl: string; + gnoUrl: string; + apiUrl: string; + linkUrl: string; + token: { + denom: string; + unit: number; + minimalDenom: string; + minimalUnit: number; + }; +}[]; + +export type CurrentChainIdModelV004 = string; + +export type CurrentNetworkIdModelV004 = string; + +export type SerializedModelV004 = string; + +export type WalletModelV004 = { + accounts: AccountDataModelV004[]; + keyrings: KeyringDataModelV004[]; + currentAccountId?: string; +}; + +type AccountDataModelV004 = { + id?: string; + index: number; + type: 'HD_WALLET' | 'PRIVATE_KEY' | 'LEDGER' | 'WEB3_AUTH'; + name: string; + keyringId: string; + hdPath?: number; + publicKey: number[]; +}; + +type KeyringDataModelV004 = { + id?: string; + type: 'HD_WALLET' | 'PRIVATE_KEY' | 'LEDGER' | 'WEB3_AUTH'; + publicKey?: number[]; + privateKey?: number[]; + seed?: number[]; + mnemonic?: string; +}; + +export type EncryptedStoredPasswordModelV004 = string; + +export type CurrentAccountIdModelV004 = string; + +export type AccountNamesModelV004 = { [key in string]: string }; + +export type EstablishSitesModelV004 = { + [key in string]: { + hostname: string; + chainId: string; + account: string; + name: string; + favicon: string | null; + establishedTime: string; + }[]; +}; + +export type AddressBookModelV004 = { + id: string; + name: string; + address: string; + createdAt: string; +}[]; + +export type AccountTokenMetainfoModelV004 = { + [key in string]: { + main: boolean; + tokenId: string; + networkId: string; + display: boolean; + type: 'gno-native' | 'grc20' | 'ibc-native' | 'ibc-tokens'; + name: string; + symbol: string; + decimals: number; + description?: string; + websiteUrl?: string; + image: string; + denom?: string; + pkgPath?: string; + originChain?: string; + originDenom?: string; + originType?: string; + path?: string; + channel?: string; + port?: string; + }[]; +}; diff --git a/packages/adena-extension/src/migrates/storage-migrator.spec.ts b/packages/adena-extension/src/migrates/storage-migrator.spec.ts index 7f643a96e..53bd9c3cb 100644 --- a/packages/adena-extension/src/migrates/storage-migrator.spec.ts +++ b/packages/adena-extension/src/migrates/storage-migrator.spec.ts @@ -67,7 +67,7 @@ describe('StorageMigrator', () => { const migrated = await migrator.migrate(current); expect(migrated).not.toBeNull(); - expect(migrated?.version).toBe(3); + expect(migrated?.version).toBe(4); expect(migrated?.data).not.toBeNull(); expect(migrated?.data.NETWORKS).toHaveLength(0); expect(migrated?.data.CURRENT_CHAIN_ID).toBe(''); @@ -89,7 +89,7 @@ describe('StorageMigrator', () => { const migrated = await migrator.migrate(current); expect(migrated).not.toBeNull(); - expect(migrated?.version).toBe(3); + expect(migrated?.version).toBe(4); expect(migrated?.data).not.toBeNull(); expect(migrated?.data.SERIALIZED).not.toBe(''); expect(migrated?.data.ADDRESS_BOOK).toHaveLength(1); diff --git a/packages/adena-extension/src/migrates/storage-migrator.ts b/packages/adena-extension/src/migrates/storage-migrator.ts index 889efa369..8c9523a0e 100644 --- a/packages/adena-extension/src/migrates/storage-migrator.ts +++ b/packages/adena-extension/src/migrates/storage-migrator.ts @@ -5,6 +5,8 @@ import { StorageModelDataV001, StorageModelV001 } from './migrations/v001/storag import { StorageMigration002 } from './migrations/v002/storage-migration-v002'; import { StorageModelV003 } from './migrations/v003/storage-model-v003'; import { StorageMigration003 } from './migrations/v003/storage-migration-v003'; +import { StorageMigration004 } from './migrations/v004/storage-migration-v004'; +import { StorageModelV004 } from './migrations/v004/storage-model-v004'; const LegacyStorageKeys = [ 'NETWORKS', @@ -127,6 +129,9 @@ export class StorageMigrator implements Migrator { } private async mappedJson(json: any) { + if (json?.version === 4) { + return json as StorageModelV004; + } if (json?.version === 3) { return json as StorageModelV003; } @@ -161,6 +166,6 @@ export class StorageMigrator implements Migrator { } static migrations(): Migration[] { - return [new StorageMigration002(), new StorageMigration003()]; + return [new StorageMigration002(), new StorageMigration003(), new StorageMigration004()]; } } diff --git a/packages/adena-extension/src/models/token-model.ts b/packages/adena-extension/src/models/token-model.ts index bb2624fb6..1298813ba 100644 --- a/packages/adena-extension/src/models/token-model.ts +++ b/packages/adena-extension/src/models/token-model.ts @@ -1,6 +1,7 @@ export interface TokenModel { main: boolean; tokenId: string; + networkId: string; display: boolean; type: 'gno-native' | 'grc20' | 'ibc-native' | 'ibc-tokens'; name: string; diff --git a/packages/adena-extension/src/pages/certify/change-network/index.tsx b/packages/adena-extension/src/pages/certify/change-network/index.tsx index 7cf4ae038..66849a6a6 100644 --- a/packages/adena-extension/src/pages/certify/change-network/index.tsx +++ b/packages/adena-extension/src/pages/certify/change-network/index.tsx @@ -13,6 +13,7 @@ import { WalletState } from '@states/index'; import { useNetwork } from '@hooks/use-network'; import { NetworkMetainfo } from '@states/network'; import AddCustomNetworkButton from '@components/change-network/add-custom-network-button/add-custom-network-button'; +import { useTokenMetainfo } from '@hooks/use-token-metainfo'; const Wrapper = styled.main` ${({ theme }) => theme.mixins.flexbox('column', 'flex-start', 'flex-start')}; @@ -70,12 +71,14 @@ export const ChangeNetwork = () => { const navigate = useNavigate(); const [loadinsgState] = useState('INIT'); const { currentNetwork, networks, changeNetwork } = useNetwork(); + const { initTokenMetainfos } = useTokenMetainfo(); const onClickNetwork = async (network: NetworkMetainfo) => { if (network.id === currentNetwork?.id) { return; } await changeNetwork(network.id); + await initTokenMetainfos(); navigate(RoutePath.Wallet); }; diff --git a/packages/adena-extension/src/pages/wallet/token-details/index.tsx b/packages/adena-extension/src/pages/wallet/token-details/index.tsx index b9caf5f26..98d1c3efc 100644 --- a/packages/adena-extension/src/pages/wallet/token-details/index.tsx +++ b/packages/adena-extension/src/pages/wallet/token-details/index.tsx @@ -24,6 +24,7 @@ import BigNumber from 'bignumber.js'; import { isGRC20TokenModel } from '@models/token-model'; import { StaticMultiTooltip } from '@components/tooltips/static-multi-tooltip'; import useHistoryData from '@hooks/use-history-data'; +import { HISTORY_FETCH_INTERVAL_TIME } from '@common/constants/interval.constant'; const Wrapper = styled.main` ${({ theme }) => theme.mixins.flexbox('column', 'flex-start', 'flex-start')}; @@ -123,8 +124,8 @@ export const TokenDetails = () => { useEffect(() => { if (currentAddress) { const historyFetchTimer = setInterval(() => { - refetch({ refetchPage: (page, index) => index === 0 }) - }, 10 * 1000); + refetch({ refetchPage: (_, index) => index === 0 }) + }, HISTORY_FETCH_INTERVAL_TIME); return () => clearInterval(historyFetchTimer); } }, [currentAddress, refetch]); @@ -156,7 +157,7 @@ export const TokenDetails = () => { }; const fetchTokenHistories = async (pageParam: number) => { - if (!currentAddress || currentNetwork.networkId !== 'test3') { + if (!currentAddress) { return { hits: 0, next: false, diff --git a/packages/adena-extension/src/pages/wallet/wallet-main/index.tsx b/packages/adena-extension/src/pages/wallet/wallet-main/index.tsx index 9cbd8c75d..d525b23d4 100644 --- a/packages/adena-extension/src/pages/wallet/wallet-main/index.tsx +++ b/packages/adena-extension/src/pages/wallet/wallet-main/index.tsx @@ -52,7 +52,7 @@ export const WalletMain = () => { } }, [currentAccount]); - const tokens = displayTokenBalances.map(tokenBalance => { + const tokens = displayTokenBalances.filter(tokenBalance => tokenBalance.display).map(tokenBalance => { return { tokenId: tokenBalance.tokenId, logo: tokenBalance.image || `${UnknownTokenIcon}`, diff --git a/packages/adena-extension/src/repositories/common/chain.ts b/packages/adena-extension/src/repositories/common/chain.ts index 60e351355..53d833e7d 100644 --- a/packages/adena-extension/src/repositories/common/chain.ts +++ b/packages/adena-extension/src/repositories/common/chain.ts @@ -25,15 +25,17 @@ export class ChainRepository { }; public getNetworks = async () => { + const defaultNetworks = await this.fetchNetworkMetainfos(); const networks = await this.localStorage .getToObject>('NETWORKS') + .then((networks) => (Array.isArray(networks) ? networks : [])) .catch(() => []); + const customNetworks = networks.filter((network) => network.default === false); if (networks.length === 0) { - const defaultNetworks = await this.fetchNetworkMetainfos(); await this.updateNetworks(defaultNetworks); return defaultNetworks; } - return networks; + return [...defaultNetworks, ...customNetworks]; }; public addNetwork = async (network: NetworkMetainfo) => { diff --git a/packages/adena-extension/src/repositories/common/mapper/network-metainfo-mapper.ts b/packages/adena-extension/src/repositories/common/mapper/network-metainfo-mapper.ts index e9a602cba..3fd65bc82 100644 --- a/packages/adena-extension/src/repositories/common/mapper/network-metainfo-mapper.ts +++ b/packages/adena-extension/src/repositories/common/mapper/network-metainfo-mapper.ts @@ -5,6 +5,7 @@ export type ChainMetainfoResponse = ChainMetainfoItem[]; export interface ChainMetainfoItem { id: string; default: boolean; + main: boolean; chainId: string; chainName: string; networkId: string; @@ -14,12 +15,6 @@ export interface ChainMetainfoItem { gnoUrl: string; apiUrl: string; linkUrl: string; - token: { - denom: string; - unit: number; - minimalDenom: string; - minimalUnit: number; - }; } export class NetworkMetainfoMapper { diff --git a/packages/adena-extension/src/repositories/common/mapper/token-mapper.ts b/packages/adena-extension/src/repositories/common/mapper/token-mapper.ts index 3bae2489f..f5dc7079d 100644 --- a/packages/adena-extension/src/repositories/common/mapper/token-mapper.ts +++ b/packages/adena-extension/src/repositories/common/mapper/token-mapper.ts @@ -20,7 +20,10 @@ export class TokenMapper { private static IMAGE_BASE_URI = 'https://raw.githubusercontent.com/onbloc/gno-token-resource/main'; - public static fromNativeTokenMetainfos(response: NativeTokenResponse): NativeTokenModel[] { + public static fromNativeTokenMetainfos( + networkId: string, + response: NativeTokenResponse, + ): NativeTokenModel[] { return response.map((token) => { const { decimals, denom, image, name, symbol, description, website_url } = token; const isGNOT = symbol === 'GNOT'; @@ -28,6 +31,7 @@ export class TokenMapper { main: isGNOT, display: isGNOT, tokenId: symbol, + networkId, type: 'gno-native', name, denom, @@ -40,13 +44,17 @@ export class TokenMapper { }); } - public static fromGRC20TokenMetainfos(response: GRC20TokenResponse): GRC20TokenModel[] { + public static fromGRC20TokenMetainfos( + networkId: string, + response: GRC20TokenResponse, + ): GRC20TokenModel[] { return response.map((token) => { const { decimals, pkg_path, image, name, symbol, description, website_url } = token; return { main: false, display: false, tokenId: pkg_path, + networkId, type: 'grc20', name, pkgPath: pkg_path, @@ -59,13 +67,17 @@ export class TokenMapper { }); } - public static fromIBCNativeMetainfos(response: IBCNativeTokenResponse): IBCNativeTokenModel[] { + public static fromIBCNativeMetainfos( + networkId: string, + response: IBCNativeTokenResponse, + ): IBCNativeTokenModel[] { return response.map((token) => { const { decimals, denom, image, name, symbol, description, website_url } = token; return { main: false, display: false, tokenId: symbol, + networkId, type: 'ibc-native', name, denom, @@ -78,13 +90,17 @@ export class TokenMapper { }); } - public static fromIBCTokenMetainfos(response: IBCTokenResponse): IBCTokenModel[] { + public static fromIBCTokenMetainfos( + networkId: string, + response: IBCTokenResponse, + ): IBCTokenModel[] { return response.map((token) => { const { website_url, origin_chain, origin_denom, origin_type, symbol } = token; return { main: false, display: false, tokenId: symbol, + networkId, websiteUrl: website_url, type: 'ibc-tokens', originChain: origin_chain, @@ -96,13 +112,14 @@ export class TokenMapper { } public static fromSearchTokensResponse( + networkId: string, response: SearchGRC20TokenResponse | null, tokenInfos?: TokenModel[], ) { if (response === null) { return []; } - return response.map((token) => this.mappedMetainfoBySearchToken(token, tokenInfos)); + return response.map((token) => this.mappedMetainfoBySearchToken(networkId, token, tokenInfos)); } private static mappedAddtionalTokenBySearchToken(searchToken: SearchGRC20Token) { @@ -117,6 +134,7 @@ export class TokenMapper { } private static mappedMetainfoBySearchToken( + networkId: string, searchToken: SearchGRC20Token, tokenInfos?: TokenModel[], ): GRC20TokenModel { @@ -126,6 +144,7 @@ export class TokenMapper { main: false, display: false, tokenId: pkgPath, + networkId, pkgPath, symbol, type: 'grc20', diff --git a/packages/adena-extension/src/repositories/common/token.ts b/packages/adena-extension/src/repositories/common/token.ts index e529d6bca..aa6956dc2 100644 --- a/packages/adena-extension/src/repositories/common/token.ts +++ b/packages/adena-extension/src/repositories/common/token.ts @@ -15,14 +15,18 @@ import { NativeTokenResponse, } from './response/token-asset-response'; import { TokenMapper } from './mapper/token-mapper'; +import { NetworkMetainfo } from '@states/network'; type LocalValueType = 'ACCOUNT_TOKEN_METAINFOS'; +const DEFAULT_TOKEN_NETWORK_ID = 'DEFAULT'; + const DEFAULT_TOKEN_METAINFOS: NativeTokenModel[] = [ { main: true, tokenId: 'Gnoland', name: 'Gnoland', + networkId: DEFAULT_TOKEN_NETWORK_ID, image: 'https://raw.githubusercontent.com/onbloc/gno-token-resource/main/gno-native/images/gnot.svg', symbol: 'GNOT', @@ -44,8 +48,6 @@ interface AppInfoResponse { } export class TokenRepository { - private static ADENA_API_URI = 'https://api.adena.app'; - private static GNO_TOKEN_RESOURCE_URI = 'https://raw.githubusercontent.com/onbloc/gno-token-resource/main'; @@ -55,9 +57,23 @@ export class TokenRepository { private networkInstance: AxiosInstance; + private networkMetainfo: NetworkMetainfo | null; + constructor(localStorage: StorageManager, networkInstance: AxiosInstance) { this.localStorage = localStorage; this.networkInstance = networkInstance; + this.networkMetainfo = null; + } + + public setNetworkMetainfo(networkMetainfo: NetworkMetainfo) { + this.networkMetainfo = networkMetainfo; + } + + private getAPIUrl() { + if (this.networkMetainfo === null || this.networkMetainfo.apiUrl === '') { + return null; + } + return `${this.networkMetainfo.apiUrl}/${this.networkMetainfo.networkId}`; } public fetchTokenMetainfos = async (): Promise => { @@ -75,22 +91,22 @@ export class TokenRepository { }; public fetchGRC20TokensBy = async (keyword: string, tokenInfos?: TokenModel[]) => { + const apiUrl = this.getAPIUrl(); + if (apiUrl === null) { + return []; + } const body = { keyword, }; const response = await this.networkInstance.post( - `${TokenRepository.ADENA_API_URI}/test3/search-grc20-tokens`, + `${apiUrl}/search-grc20-tokens`, body, ); - return TokenMapper.fromSearchTokensResponse(response.data, tokenInfos); - }; - - public getAllTokenMetainfos = async (): Promise<{ [key in string]: TokenModel[] }> => { - const accountTokenMetainfos = await this.localStorage.getToObject< - { [key in string]: TokenModel[] } - >('ACCOUNT_TOKEN_METAINFOS'); - - return accountTokenMetainfos; + return TokenMapper.fromSearchTokensResponse( + this.networkMetainfo?.networkId || '', + response.data, + tokenInfos, + ); }; public getAccountTokenMetainfos = async (accountId: string): Promise => { @@ -109,8 +125,12 @@ export class TokenRepository { { [key in string]: TokenModel[] } >('ACCOUNT_TOKEN_METAINFOS'); + const isUnique = function (token0: TokenModel, token1: TokenModel) { + return token0.tokenId === token1.tokenId && token0.networkId === token1.networkId; + }; + const filteredTokenMetainfos = tokenMetainfos.filter((info1, index) => { - return tokenMetainfos.findIndex((info2) => info1.tokenId === info2.tokenId) === index; + return tokenMetainfos.findIndex((info2) => isUnique(info1, info2)) === index; }); const changedAccountTokenMetainfos = { @@ -145,7 +165,9 @@ export class TokenRepository { const requestUri = TokenRepository.GNO_TOKEN_RESOURCE_URI + '/gno-native/assets.json'; return this.networkInstance .get(requestUri) - .then((response) => TokenMapper.fromNativeTokenMetainfos(response.data)) + .then((response) => + TokenMapper.fromNativeTokenMetainfos(DEFAULT_TOKEN_NETWORK_ID, response.data), + ) .catch(() => DEFAULT_TOKEN_METAINFOS); }; @@ -153,7 +175,9 @@ export class TokenRepository { const requestUri = TokenRepository.GNO_TOKEN_RESOURCE_URI + '/grc20/assets.json'; return this.networkInstance .get(requestUri) - .then((response) => TokenMapper.fromGRC20TokenMetainfos(response.data)) + .then((response) => + TokenMapper.fromGRC20TokenMetainfos(DEFAULT_TOKEN_NETWORK_ID, response.data), + ) .catch(() => []); }; @@ -161,7 +185,9 @@ export class TokenRepository { const requestUri = TokenRepository.GNO_TOKEN_RESOURCE_URI + '/ibc-native/assets.json'; return this.networkInstance .get(requestUri) - .then((response) => TokenMapper.fromIBCNativeMetainfos(response.data)) + .then((response) => + TokenMapper.fromIBCNativeMetainfos(DEFAULT_TOKEN_NETWORK_ID, response.data), + ) .catch(() => []); }; @@ -169,7 +195,9 @@ export class TokenRepository { const requestUri = TokenRepository.GNO_TOKEN_RESOURCE_URI + '/ibc-tokens/assets.json'; return this.networkInstance .get(requestUri) - .then((response) => TokenMapper.fromIBCTokenMetainfos(response.data)) + .then((response) => + TokenMapper.fromIBCTokenMetainfos(DEFAULT_TOKEN_NETWORK_ID, response.data), + ) .catch(() => []); }; } diff --git a/packages/adena-extension/src/repositories/transaction/transaction-history.ts b/packages/adena-extension/src/repositories/transaction/transaction-history.ts index 6322e4666..966f52816 100644 --- a/packages/adena-extension/src/repositories/transaction/transaction-history.ts +++ b/packages/adena-extension/src/repositories/transaction/transaction-history.ts @@ -1,18 +1,40 @@ import { AxiosInstance } from 'axios'; import { TransactionHistoryResponse } from './response/transaction-history-response'; import { TransactionHistoryMapper } from './mapper/transaction-history-mapper'; +import { NetworkMetainfo } from '@states/network'; export class TransactionHistoryRepository { - private static ADENA_API_URI = 'https://api.adena.app'; - private axiosInstance: AxiosInstance; + private networkMetainfo: NetworkMetainfo | null; + constructor(axiosInstance: AxiosInstance) { this.axiosInstance = axiosInstance; + this.networkMetainfo = null; + } + + private getAPIUrl() { + if (this.networkMetainfo === null || this.networkMetainfo.apiUrl === '') { + return null; + } + return `${this.networkMetainfo.apiUrl}/${this.networkMetainfo.networkId}`; + } + + public setNetworkMetainfo(networkMetaion: NetworkMetainfo) { + this.networkMetainfo = networkMetaion; } public async fetchAllTransactionHistoryBy(address: string, from: number, size?: number) { - const requestUri = `${TransactionHistoryRepository.ADENA_API_URI}/test3/multi_history/${address}`; + const apiUri = this.getAPIUrl(); + if (!apiUri) { + return { + hits: 0, + next: false, + txs: [], + }; + } + + const requestUri = `${apiUri}/multi_history/${address}`; const response = await this.axiosInstance.get(requestUri, { params: { from, @@ -23,7 +45,15 @@ export class TransactionHistoryRepository { } public async fetchNativeTransactionHistoryBy(address: string, from: number, size?: number) { - const requestUri = `${TransactionHistoryRepository.ADENA_API_URI}/test3/native-token-history/${address}`; + const apiUri = this.getAPIUrl(); + if (!apiUri) { + return { + hits: 0, + next: false, + txs: [], + }; + } + const requestUri = `${apiUri}/native-token-history/${address}`; const response = await this.axiosInstance.get(requestUri, { params: { from, @@ -39,7 +69,15 @@ export class TransactionHistoryRepository { from: number, size?: number, ) { - const requestUri = `${TransactionHistoryRepository.ADENA_API_URI}/test3/grc20-token-history/${address}/${packagePath}`; + const apiUri = this.getAPIUrl(); + if (!apiUri) { + return { + hits: 0, + next: false, + txs: [], + }; + } + const requestUri = `${apiUri}/grc20-token-history/${address}/${packagePath}`; const response = await this.axiosInstance.get(requestUri, { params: { from, diff --git a/packages/adena-extension/src/resources/chains/chains.json b/packages/adena-extension/src/resources/chains/chains.json index 9b0cac0cf..c0eb498b0 100644 --- a/packages/adena-extension/src/resources/chains/chains.json +++ b/packages/adena-extension/src/resources/chains/chains.json @@ -2,6 +2,7 @@ { "id": "test3", "default": true, + "main": true, "chainId": "test3", "chainName": "GNO.LAND", "networkId": "test3", @@ -10,17 +11,12 @@ "rpcUrl": "https://rpc.test3.gno.land", "gnoUrl": "https://test3.gno.land", "apiUrl": "https://api.adena.app", - "linkUrl": "https://gnoscan.io", - "token": { - "denom": "gnot", - "unit": 1, - "minimalDenom": "ugnot", - "minimalUnit": 0.000001 - } + "linkUrl": "https://gnoscan.io" }, { "id": "test2", - "default": false, + "default": true, + "main": false, "chainId": "test2", "chainName": "GNO.LAND", "networkId": "test2", @@ -28,18 +24,13 @@ "addressPrefix": "g", "rpcUrl": "https://rpc.test2.gno.land", "gnoUrl": "https://test2.gno.land", - "apiUrl": "https://api.adena.app", - "linkUrl": "https://test2.gnoscan.io", - "token": { - "denom": "gnot", - "unit": 1, - "minimalDenom": "ugnot", - "minimalUnit": 0.000001 - } + "apiUrl": "", + "linkUrl": "https://test2.gnoscan.io" }, { "id": "dev", - "default": false, + "default": true, + "main": false, "chainId": "dev", "chainName": "GNO.LAND", "networkId": "dev", @@ -47,13 +38,7 @@ "addressPrefix": "g", "rpcUrl": "http://127.0.0.1:26657", "gnoUrl": "http://127.0.0.1:8888", - "apiUrl": "http://127.0.0.1:8080", - "linkUrl": "http://127.0.0.1:80", - "token": { - "denom": "gnot", - "unit": 1, - "minimalDenom": "ugnot", - "minimalUnit": 0.000001 - } + "apiUrl": "http://127.0.0.1:3333", + "linkUrl": "http://127.0.0.1:3000" } ] diff --git a/packages/adena-extension/src/services/resource/chain.ts b/packages/adena-extension/src/services/resource/chain.ts index 78e79aa52..e7cc9fbfc 100644 --- a/packages/adena-extension/src/services/resource/chain.ts +++ b/packages/adena-extension/src/services/resource/chain.ts @@ -31,6 +31,7 @@ export class ChainService { const addedNetwork = { id: `${Date.now()}`, default: false, + main: false, chainId: chainId, chainName: 'GNO.LAND', networkId: chainId, diff --git a/packages/adena-extension/src/services/resource/token.ts b/packages/adena-extension/src/services/resource/token.ts index 7353ab46e..f88a36c72 100644 --- a/packages/adena-extension/src/services/resource/token.ts +++ b/packages/adena-extension/src/services/resource/token.ts @@ -1,6 +1,7 @@ import { TokenModel, isGRC20TokenModel, isNativeTokenModel } from '@models/token-model'; import { TokenRepository } from '@repositories/common'; import { AccountTokenBalance } from '@states/balance'; +import { NetworkMetainfo } from '@states/network'; export class TokenService { private tokenRepository: TokenRepository; @@ -12,6 +13,10 @@ export class TokenService { this.tokenMetaInfos = []; } + public setNetworkMetainfo(networkMetainfo: NetworkMetainfo) { + this.tokenRepository.setNetworkMetainfo(networkMetainfo); + } + public getTokenMetainfos() { return this.tokenMetaInfos; } diff --git a/packages/adena-extension/src/services/transaction/transaction-history.ts b/packages/adena-extension/src/services/transaction/transaction-history.ts index 4c0b8f4c6..e346e7026 100644 --- a/packages/adena-extension/src/services/transaction/transaction-history.ts +++ b/packages/adena-extension/src/services/transaction/transaction-history.ts @@ -1,4 +1,5 @@ import { TransactionHistoryRepository } from '@repositories/transaction'; +import { NetworkMetainfo } from '@states/network'; export class TransactionHistoryService { private transactionHisotyrRepository: TransactionHistoryRepository; @@ -7,6 +8,10 @@ export class TransactionHistoryService { this.transactionHisotyrRepository = transactionHisotyrRepository; } + public setNetworkMetainfo(networkMetainfo: NetworkMetainfo) { + return this.transactionHisotyrRepository.setNetworkMetainfo(networkMetainfo); + } + public fetchAllTransactionHistory(address: string, from: number, size?: number) { return this.transactionHisotyrRepository.fetchAllTransactionHistoryBy(address, from, size); } diff --git a/packages/adena-extension/src/states/network.ts b/packages/adena-extension/src/states/network.ts index 1b47bfff5..5422ab668 100644 --- a/packages/adena-extension/src/states/network.ts +++ b/packages/adena-extension/src/states/network.ts @@ -3,6 +3,7 @@ import { atom } from 'recoil'; export interface NetworkMetainfo { id: string; default: boolean; + main?: boolean; chainId: string; chainName: string; networkId: string; @@ -12,12 +13,6 @@ export interface NetworkMetainfo { gnoUrl: string; apiUrl: string; linkUrl: string; - token: { - denom: string; - unit: number; - minimalDenom: string; - minimalUnit: number; - }; } export const networkMetainfos = atom({