diff --git a/packages/adena-extension/src/hooks/use-network.ts b/packages/adena-extension/src/hooks/use-network.ts index 7e54de46..fa54a4eb 100644 --- a/packages/adena-extension/src/hooks/use-network.ts +++ b/packages/adena-extension/src/hooks/use-network.ts @@ -1,15 +1,15 @@ import { useCallback, useMemo } from 'react'; import { useRecoilState, useResetRecoilState } from 'recoil'; -import { useAdenaContext, useWalletContext } from './use-context'; +import { fetchHealth } from '@common/utils/fetch-utils'; import { EventMessage } from '@inject/message'; +import { useAdenaContext, useWalletContext } from './use-context'; import { useEvent } from './use-event'; -import { fetchHealth } from '@common/utils/fetch-utils'; -import { NetworkMetainfo } from '@types'; +import CHAIN_DATA from '@resources/chains/chains.json'; import { BalanceState, NetworkState, WalletState } from '@states'; import { useQuery } from '@tanstack/react-query'; -import CHAIN_DATA from '@resources/chains/chains.json'; +import { NetworkMetainfo } from '@types'; interface NetworkResponse { networks: NetworkMetainfo[]; @@ -54,8 +54,11 @@ export const useNetwork = (): NetworkResponse => { if (!currentNetwork) { return null; } - const isCustomNetwork = ['test4', 'portal-loop'].includes(currentNetwork.networkId); - const networkParameters: { [key in string]: string } = isCustomNetwork + const officialNetworkIds = CHAIN_DATA.filter((network) => !!network.apiUrl).map( + (network) => network.networkId, + ); + const isOfficialNetwork = officialNetworkIds.includes(currentNetwork.networkId); + const networkParameters: { [key in string]: string } = isOfficialNetwork ? { chainId: currentNetwork.networkId, } diff --git a/packages/adena-extension/src/migrates/migrations/v008/storage-migration-v008.spec.ts b/packages/adena-extension/src/migrates/migrations/v008/storage-migration-v008.spec.ts new file mode 100644 index 00000000..f2d91753 --- /dev/null +++ b/packages/adena-extension/src/migrates/migrations/v008/storage-migration-v008.spec.ts @@ -0,0 +1,76 @@ +import { decryptAES } from 'adena-module'; +import { StorageMigration008 } from './storage-migration-v008'; + +const mockStorageData = { + NETWORKS: [], + CURRENT_CHAIN_ID: 'test4', + CURRENT_NETWORK_ID: 'test4', + SERIALIZED: 'U2FsdGVkX19eI8kOCI/T9o1Ru0b2wdj5rHxmG4QbLQ0yZH4kDa8/gg6Ac2JslvEm', + ENCRYPTED_STORED_PASSWORD: '', + CURRENT_ACCOUNT_ID: '', + ACCOUNT_NAMES: {}, + ESTABLISH_SITES: {}, + ADDRESS_BOOK: '', + ACCOUNT_TOKEN_METAINFOS: {}, + QUESTIONNAIRE_EXPIRED_DATE: null, + WALLET_CREATION_GUIDE_CONFIRM_DATE: null, + ADD_ACCOUNT_GUIDE_CONFIRM_DATE: null, + ACCOUNT_GRC721_COLLECTIONS: {}, + ACCOUNT_GRC721_PINNED_PACKAGES: {}, +}; + +describe('serialized wallet migration V008', () => { + it('version', () => { + const migration = new StorageMigration008(); + expect(migration.version).toBe(8); + }); + + it('up success', async () => { + const mockData = { + version: 7, + data: mockStorageData, + }; + const migration = new StorageMigration008(); + const result = await migration.up(mockData); + + expect(result.data.CURRENT_CHAIN_ID).toEqual('test5'); + expect(result.data.CURRENT_NETWORK_ID).toEqual('test5'); + }); + + it('up password success', async () => { + const mockData = { + version: 1, + data: mockStorageData, + }; + const password = '123'; + const migration = new StorageMigration008(); + const result = await migration.up(mockData); + + expect(result.version).toBe(8); + expect(result.data).not.toBeNull(); + expect(result.data.ACCOUNT_GRC721_COLLECTIONS).toEqual({}); + expect(result.data.ACCOUNT_GRC721_PINNED_PACKAGES).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); + + expect(result.data.CURRENT_CHAIN_ID).toEqual('test5'); + expect(result.data.CURRENT_NETWORK_ID).toEqual('test5'); + }); + + it('up failed throw error', async () => { + const mockData: any = { + version: 1, + data: { ...mockStorageData, SERIALIZED: null }, + }; + const migration = new StorageMigration008(); + + await expect(migration.up(mockData)).rejects.toThrow( + 'Storage Data does not match version V007', + ); + }); +}); diff --git a/packages/adena-extension/src/migrates/migrations/v008/storage-migration-v008.ts b/packages/adena-extension/src/migrates/migrations/v008/storage-migration-v008.ts new file mode 100644 index 00000000..31133c77 --- /dev/null +++ b/packages/adena-extension/src/migrates/migrations/v008/storage-migration-v008.ts @@ -0,0 +1,124 @@ +import { StorageModel } from '@common/storage'; +import { Migration } from '@migrates/migrator'; +import CHAIN_DATA from '@resources/chains/chains.json'; +import { + CurrentChainIdModelV007, + CurrentNetworkIdModelV007, + NetworksModelV007, + StorageModelDataV007, +} from '../v007/storage-model-v007'; +import { + CurrentChainIdModelV008, + CurrentNetworkIdModelV008, + NetworksModelV008, + StorageModelDataV008, +} from './storage-model-v008'; + +export class StorageMigration008 implements Migration { + public readonly version = 8; + + async up( + current: StorageModel, + ): Promise> { + if (!this.validateModelV007(current.data)) { + throw new Error('Storage Data does not match version V007'); + } + const previous: StorageModelDataV007 = current.data; + return { + version: this.version, + data: { + ...previous, + CURRENT_CHAIN_ID: this.migrateCurrentChainId(previous.CURRENT_CHAIN_ID), + CURRENT_NETWORK_ID: this.migrateCurrentNetworkId(previous.CURRENT_NETWORK_ID), + NETWORKS: this.migrateNetwork(previous.NETWORKS), + }, + }; + } + + private validateModelV007(currentData: StorageModelDataV007): boolean { + const storageDataKeys = [ + 'NETWORKS', + 'CURRENT_CHAIN_ID', + 'CURRENT_NETWORK_ID', + 'SERIALIZED', + 'ENCRYPTED_STORED_PASSWORD', + 'CURRENT_ACCOUNT_ID', + 'ESTABLISH_SITES', + 'ADDRESS_BOOK', + 'ACCOUNT_TOKEN_METAINFOS', + 'ACCOUNT_GRC721_COLLECTIONS', + 'ACCOUNT_GRC721_PINNED_PACKAGES', + ]; + const currentDataKeys = Object.keys(currentData); + const hasKeys = storageDataKeys.every((dataKey) => { + return currentDataKeys.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 (currentData.ACCOUNT_NAMES && typeof currentData.ACCOUNT_NAMES !== 'object') { + return false; + } + if (currentData.ESTABLISH_SITES && typeof currentData.ESTABLISH_SITES !== 'object') { + return false; + } + return true; + } + + private migrateCurrentChainId(currentChainId: CurrentChainIdModelV007): CurrentChainIdModelV008 { + if (currentChainId === 'test4') { + return 'test5'; + } + return currentChainId; + } + + private migrateCurrentNetworkId( + currentNetworkId: CurrentNetworkIdModelV007, + ): CurrentNetworkIdModelV008 { + if (currentNetworkId === 'test4') { + return 'test5'; + } + return currentNetworkId; + } + + private migrateNetwork(networks: NetworksModelV007): NetworksModelV008 { + const defaultNetworks = CHAIN_DATA.filter((network) => network.default); + const customNetworks = networks + .filter((network) => !network.default) + .map((network) => { + const providedNetwork = CHAIN_DATA.find((data) => data.chainId === network.id); + if (providedNetwork) { + return { + ...providedNetwork, + chainName: network.chainName, + networkName: network.networkName, + rpcUrl: network.rpcUrl, + }; + } + return { + ...network, + indexerUrl: '', + }; + }); + return [...defaultNetworks, ...customNetworks]; + } +} diff --git a/packages/adena-extension/src/migrates/migrations/v008/storage-model-v008.ts b/packages/adena-extension/src/migrates/migrations/v008/storage-model-v008.ts new file mode 100644 index 00000000..b59f0b05 --- /dev/null +++ b/packages/adena-extension/src/migrates/migrations/v008/storage-model-v008.ts @@ -0,0 +1,145 @@ +export type StorageModelV008 = { + version: 8; + data: StorageModelDataV008; +}; + +export type StorageModelDataV008 = { + NETWORKS: NetworksModelV008; + CURRENT_CHAIN_ID: CurrentChainIdModelV008; + CURRENT_NETWORK_ID: CurrentNetworkIdModelV008; + SERIALIZED: SerializedModelV008; + ENCRYPTED_STORED_PASSWORD: EncryptedStoredPasswordModelV008; + CURRENT_ACCOUNT_ID: CurrentAccountIdModelV008; + ACCOUNT_NAMES: AccountNamesModelV008; + ESTABLISH_SITES: EstablishSitesModelV008; + ADDRESS_BOOK: AddressBookModelV008; + ACCOUNT_TOKEN_METAINFOS: AccountTokenMetainfoModelV008; + QUESTIONNAIRE_EXPIRED_DATE: QuestionnaireExpiredDateModelV008; + WALLET_CREATION_GUIDE_CONFIRM_DATE: WalletCreationGuideConfirmDateModelV008; + ADD_ACCOUNT_GUIDE_CONFIRM_DATE: AddAccountGuideConfirmDateModelV008; + ACCOUNT_GRC721_COLLECTIONS: AccountGRC721CollectionsV008; + ACCOUNT_GRC721_PINNED_PACKAGES: AccountGRC721PinnedPackagesV008; +}; + +export type NetworksModelV008 = { + id: string; + default: boolean; + main: boolean; + chainId: string; + chainName: string; + networkId: string; + networkName: string; + addressPrefix: string; + rpcUrl: string; + indexerUrl: string; + gnoUrl: string; + apiUrl: string; + linkUrl: string; + deleted?: boolean; +}[]; + +export type CurrentChainIdModelV008 = string; + +export type CurrentNetworkIdModelV008 = string; + +export type SerializedModelV008 = string; + +export type QuestionnaireExpiredDateModelV008 = number | null; + +export type WalletCreationGuideConfirmDateModelV008 = number | null; + +export type AddAccountGuideConfirmDateModelV008 = number | null; + +export type WalletModelV008 = { + accounts: AccountDataModelV008[]; + keyrings: KeyringDataModelV008[]; + currentAccountId?: string; +}; + +type AccountDataModelV008 = { + id?: string; + index: number; + type: 'HD_WALLET' | 'PRIVATE_KEY' | 'LEDGER' | 'WEB3_AUTH' | 'AIRGAP'; + name: string; + keyringId: string; + hdPath?: number; + publicKey: number[]; + addressBytes?: number[]; +}; + +type KeyringDataModelV008 = { + id?: string; + type: 'HD_WALLET' | 'PRIVATE_KEY' | 'LEDGER' | 'WEB3_AUTH' | 'AIRGAP'; + publicKey?: number[]; + privateKey?: number[]; + seed?: number[]; + mnemonic?: string; + addressBytes?: number[]; +}; + +export type EncryptedStoredPasswordModelV008 = string; + +export type CurrentAccountIdModelV008 = string; + +type AccountId = string; +type NetworkId = string; + +export type AccountNamesModelV008 = { [key in AccountId]: string }; + +export type EstablishSitesModelV008 = { + [key in AccountId]: { + hostname: string; + chainId: string; + account: string; + name: string; + favicon: string | null; + establishedTime: string; + }[]; +}; + +export type AddressBookModelV008 = string; + +export type AccountTokenMetainfoModelV008 = { + [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; + }[]; +}; + +export type AccountGRC721CollectionsV008 = { + [key in AccountId]: { + [key in NetworkId]: { + tokenId: string; + networkId: string; + display: boolean; + type: 'grc721'; + packagePath: string; + name: string; + symbol: string; + image: string | null; + isTokenUri: boolean; + isMetadata: boolean; + }[]; + }; +}; + +export type AccountGRC721PinnedPackagesV008 = { + [key in AccountId]: { [key in NetworkId]: string[] }; +};