diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e1590720..85525e6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes +## v1.7.3 + +### Features + +- [#1058](https://github.com/alleslabs/celatone-frontend/pull/1058) Show chain config JSON and remove modal +- [#1057](https://github.com/alleslabs/celatone-frontend/pull/1057) Add local minitias to network selector +- [#1043](https://github.com/alleslabs/celatone-frontend/pull/1043) Save custom minitias JSON form to localstorage +- [#1059](https://github.com/alleslabs/celatone-frontend/pull/1059) Disable add custom networks on non initia deployments +- [#1052](https://github.com/alleslabs/celatone-frontend/pull/1052) Add entry point to add custom minitias network in network selector +- [#1038](https://github.com/alleslabs/celatone-frontend/pull/1038) Add custom networks options page to navigate between manual and upload json +- [#1054](https://github.com/alleslabs/celatone-frontend/pull/1054) Add chain config store, useChainConfig hook, and apply it all places + +### Improvements + +- [#1062](https://github.com/alleslabs/celatone-frontend/pull/1062) Adjust edit minitia layout and other minor styling +- [#1061](https://github.com/alleslabs/celatone-frontend/pull/1061) Refactor custom network routes and add support chain ids to hook +- [#1053](https://github.com/alleslabs/celatone-frontend/pull/1053) Move move decoder and verifier links to env + +### Bug fixes + +- [#1063](https://github.com/alleslabs/celatone-frontend/pull/1063) Fix bug bash for add custom networks +- [#1069](https://github.com/alleslabs/celatone-frontend/pull/1069) Fix add custom minitia network manually +- [#1065](https://github.com/alleslabs/celatone-frontend/pull/1065) Fix zod url validation to allow only http and https in add custom minitia page +- [#1064](https://github.com/alleslabs/celatone-frontend/pull/1064) Fix cancel button in add custom minitia page +- [#1068](https://github.com/alleslabs/celatone-frontend/pull/1068) Add fetching bech32 prefix from lcd and disable close success modal on overlay click +- [#1067](https://github.com/alleslabs/celatone-frontend/pull/1067) Add gas fee details to support more decimal digits and add default value +- [#1066](https://github.com/alleslabs/celatone-frontend/pull/1066) Remove add custom minitia supported feature step and add vm type in network detail +- [#1050](https://github.com/alleslabs/celatone-frontend/pull/1050) Fix gap between network subsection + ## v1.7.2 ### Features diff --git a/package.json b/package.json index cefec5ea9..11cd2d2df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "celatone", - "version": "1.7.2", + "version": "1.7.3", "author": "Alles Labs", "contributors": [ { @@ -42,10 +42,11 @@ "build-storybook": "storybook build" }, "dependencies": { + "@alleslabs/shared": "1.0.0-dev2", "@amplitude/analytics-browser": "^2.3.3", "@amplitude/analytics-types": "^2.3.0", "@amplitude/plugin-user-agent-enrichment-browser": "^1.0.0", - "@chain-registry/types": "0.17.0", + "@chain-registry/types": "0.45.36", "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/card": "^2.2.0", "@chakra-ui/icons": "^2.1.1", @@ -72,6 +73,7 @@ "@emotion/styled": "^11.11.0", "@graphql-codegen/cli": "^5.0.0", "@graphql-typed-document-node/core": "^3.2.0", + "@hookform/resolvers": "^3.9.0", "@initia/initia.js": "0.2.5", "@initia/initia.proto": "0.2.0", "@initia/utils": "0.62.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee6226010..2efd576a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@alleslabs/shared': + specifier: 1.0.0-dev2 + version: 1.0.0-dev2 '@amplitude/analytics-browser': specifier: ^2.3.3 version: 2.3.8 @@ -15,8 +18,8 @@ dependencies: specifier: ^1.0.0 version: 1.0.1 '@chain-registry/types': - specifier: 0.17.0 - version: 0.17.0 + specifier: 0.45.36 + version: 0.45.36 '@chakra-ui/anatomy': specifier: ^2.2.2 version: 2.2.2 @@ -70,10 +73,10 @@ dependencies: version: 2.15.0(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@interchain-ui/react@1.23.9)(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) '@cosmos-kit/station': specifier: 2.9.0 - version: 2.9.0(@chain-registry/types@0.17.0)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5) + version: 2.9.0(@chain-registry/types@0.45.36)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5) '@cosmos-kit/station-extension': specifier: 2.10.0 - version: 2.10.0(@chain-registry/types@0.17.0)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5) + version: 2.10.0(@chain-registry/types@0.45.36)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5) '@dnd-kit/core': specifier: ^6.1.0 version: 6.1.0(react-dom@18.2.0)(react@18.2.0) @@ -95,6 +98,9 @@ dependencies: '@graphql-typed-document-node/core': specifier: ^3.2.0 version: 3.2.0(graphql@16.8.1) + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.49.3) '@initia/initia.js': specifier: 0.2.5 version: 0.2.5 @@ -220,13 +226,13 @@ dependencies: version: 6.3.3 mobx: specifier: ^6.6.2 - version: 6.12.0 + version: 6.13.1 mobx-persist-store: specifier: ^1.1.2 - version: 1.1.4(mobx@6.12.0) + version: 1.1.5(mobx@6.13.1) mobx-react-lite: specifier: ^3.4.0 - version: 3.4.3(mobx@6.12.0)(react-dom@18.2.0)(react@18.2.0) + version: 3.4.3(mobx@6.13.1)(react-dom@18.2.0)(react@18.2.0) monaco-editor: specifier: ^0.44.0 version: 0.44.0 @@ -407,6 +413,12 @@ packages: resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} dev: false + /@alleslabs/shared@1.0.0-dev2: + resolution: {integrity: sha512-f2zRLNVkkSdIrF1F9xd14bLLCqtr5KYChLVD4cKHUUHXgHdr0i1ZZYClBroHK7uKx2W8tAecOFrksPMrcng/0A==} + dependencies: + '@chain-registry/types': 0.45.36 + dev: false + /@amplitude/analytics-browser@2.3.8: resolution: {integrity: sha512-K+12aAVJPzAtWIi8Ok5Q5dvg7v7IF4G0cI8PW0COWo3uTyY103r45OcpgrpRVpVAr+41d1eiMo36jqOke89uPA==} dependencies: @@ -1867,6 +1879,10 @@ packages: resolution: {integrity: sha512-EHow6xgTM0pe5mfm7kp1ApbJI83Bb0vXaG+LBxLcGXC6VfqCIVcT7Ki5PsdbYfHrfekoH1urXORUJo51F4ihQA==} dev: false + /@chain-registry/types@0.45.36: + resolution: {integrity: sha512-HEKthtP24qy7hQlpcTNXidACYO6uwMNwZaV0O8Rv0fH226syrgH7nnGWofeooHGPofRFJfAfmpGJailBWuhWAQ==} + dev: false + /@chain-registry/utils@1.45.4: resolution: {integrity: sha512-N0702U78CkucKA2e/g19/GFn9wEEJAeFvlDuX+vZ2cMmgAUuSCNXpYLV55crPaPq/s4RTeulJL7DVBb+irQQsg==} dependencies: @@ -3647,14 +3663,14 @@ packages: - utf-8-validate dev: false - /@cosmos-kit/station-extension@2.10.0(@chain-registry/types@0.17.0)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5): + /@cosmos-kit/station-extension@2.10.0(@chain-registry/types@0.45.36)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5): resolution: {integrity: sha512-5UeGRb4C1MSmiWcrHJnjeIzHyyAlEszJabPeE92FqMacReCTksPplVAL0sdZeQ8CzWTV3JhEPF4NYQSB8gRhVg==} peerDependencies: '@chain-registry/types': '>= 0.17' '@cosmjs/amino': '>=0.32.3' '@cosmjs/proto-signing': '>=0.32.3' dependencies: - '@chain-registry/types': 0.17.0 + '@chain-registry/types': 0.45.36 '@cosmjs/amino': 0.32.3 '@cosmjs/proto-signing': 0.32.3 '@cosmos-kit/core': 2.12.0 @@ -3683,10 +3699,10 @@ packages: - utf-8-validate dev: false - /@cosmos-kit/station@2.9.0(@chain-registry/types@0.17.0)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5): + /@cosmos-kit/station@2.9.0(@chain-registry/types@0.45.36)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5): resolution: {integrity: sha512-JkNh+14I8ZSsxbzsYTMjt0sQ2pVcwNmhMGo7jRuF/FA0x1Lf6+V55Bp3z5uM4vpOuKrM8vWo8izp0le247U2SQ==} dependencies: - '@cosmos-kit/station-extension': 2.10.0(@chain-registry/types@0.17.0)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5) + '@cosmos-kit/station-extension': 2.10.0(@chain-registry/types@0.45.36)(@cosmjs/amino@0.32.3)(@cosmjs/proto-signing@0.32.3)(@terra-money/terra.js@3.1.10)(axios@1.6.5) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -4992,6 +5008,14 @@ packages: graphql: 16.8.1 dev: false + /@hookform/resolvers@3.9.0(react-hook-form@7.49.3): + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + dependencies: + react-hook-form: 7.49.3(react@18.2.0) + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -5511,7 +5535,7 @@ packages: '@keplr-wallet/types': 0.12.28 buffer: 6.0.3 delay: 4.4.1 - mobx: 6.12.0 + mobx: 6.13.1 dev: false /@keplr-wallet/cosmos@0.12.28: @@ -17341,15 +17365,15 @@ packages: ufo: 1.3.2 dev: false - /mobx-persist-store@1.1.4(mobx@6.12.0): - resolution: {integrity: sha512-kGdTpnpfvTC61XiqlYnAN+gvkGiYfLgGpV1nj6k8hHom8V+vrPJyYMg+V1Rc5dNi7sa+qrHlbZfCvbaJoAtpSg==} + /mobx-persist-store@1.1.5(mobx@6.13.1): + resolution: {integrity: sha512-STVTfYErC4Vw58zbAiz/Em4bmwi2sa+zXudxHwRtJvwiEejrJ6W1FcFDRskZyjrGLNOcrT0dDK2VDplV2cgQig==} peerDependencies: mobx: '*' dependencies: - mobx: 6.12.0 + mobx: 6.13.1 dev: false - /mobx-react-lite@3.4.3(mobx@6.12.0)(react-dom@18.2.0)(react@18.2.0): + /mobx-react-lite@3.4.3(mobx@6.13.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==} peerDependencies: mobx: ^6.1.0 @@ -17362,13 +17386,13 @@ packages: react-native: optional: true dependencies: - mobx: 6.12.0 + mobx: 6.13.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /mobx@6.12.0: - resolution: {integrity: sha512-Mn6CN6meXEnMa0a5u6a5+RKrqRedHBhZGd15AWLk9O6uFY4KYHzImdt8JI8WODo1bjTSRnwXhJox+FCUZhCKCQ==} + /mobx@6.13.1: + resolution: {integrity: sha512-ekLRxgjWJr8hVxj9ZKuClPwM/iHckx3euIJ3Np7zLVNtqJvfbbq7l370W/98C8EabdQ1pB5Jd3BbDWxJPNnaOg==} dev: false /mockdate@3.0.5: diff --git a/src/config/chain/initia.ts b/src/config/chain/initia.ts index 71278c2f4..9708f5b0b 100644 --- a/src/config/chain/initia.ts +++ b/src/config/chain/initia.ts @@ -4,9 +4,6 @@ import { wallets as keplrWallets } from "@cosmos-kit/keplr"; import type { ChainConfigs } from "./types"; -const INITIA_DECODER = - "https://initia-api-jiod42ec2q-as.a.run.app/decode_module"; - export const INITIA_CHAIN_CONFIGS: ChainConfigs = { "minimove-1-lite": { tier: "lite", @@ -29,8 +26,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, @@ -80,8 +75,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, @@ -131,8 +124,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "https://compiler.initiation-1.initia.xyz/contracts/verify", }, pool: { enabled: false, @@ -184,8 +175,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, @@ -285,8 +274,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, @@ -336,8 +323,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, @@ -387,8 +372,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, @@ -438,8 +421,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, @@ -488,8 +469,6 @@ export const INITIA_CHAIN_CONFIGS: ChainConfigs = { move: { enabled: true, moduleMaxFileSize: 1_048_576, - decodeApi: INITIA_DECODER, - verify: "", }, pool: { enabled: false, diff --git a/src/config/chain/types.ts b/src/config/chain/types.ts index 525fcfb97..13fb6fd9c 100644 --- a/src/config/chain/types.ts +++ b/src/config/chain/types.ts @@ -23,8 +23,6 @@ type MoveConfig = | { enabled: true; moduleMaxFileSize: number; - decodeApi: string; - verify: string; } | { enabled: false }; @@ -58,7 +56,7 @@ export interface ChainConfig { registryChainName: string; prettyName: string; logoUrl?: string; - networkType: "mainnet" | "testnet"; + networkType: "mainnet" | "testnet" | "devnet" | "local"; lcd: string; rpc: string; indexer: string; diff --git a/src/env.ts b/src/env.ts index 8b0824ecf..2f687c66c 100644 --- a/src/env.ts +++ b/src/env.ts @@ -29,3 +29,9 @@ export const CELATONE_API_OVERRIDE = export const HASURA_ADMIN_SECRET = process.env.NEXT_PUBLIC_HASURA_ADMIN_SECRET ?? ""; + +export const INITIA_MOVE_DECODER = + process.env.NEXT_PUBLIC_INITIA_MOVE_DECODER ?? ""; + +export const INITIA_MOVE_VERIFIER = + process.env.NEXT_PUBLIC_INITIA_MOVE_VERIFIER ?? ""; diff --git a/src/lib/app-provider/contexts/app.tsx b/src/lib/app-provider/contexts/app.tsx index 3768185bd..b7426e136 100644 --- a/src/lib/app-provider/contexts/app.tsx +++ b/src/lib/app-provider/contexts/app.tsx @@ -1,5 +1,6 @@ import { useModalTheme } from "@cosmos-kit/react"; import { GraphQLClient } from "graphql-request"; +import { observer } from "mobx-react-lite"; import type { ReactNode } from "react"; import { createContext, @@ -9,11 +10,12 @@ import { useState, } from "react"; +import { useChainConfigs } from "../hooks/useChainConfigs"; import { useNetworkChange } from "../hooks/useNetworkChange"; -import { CHAIN_CONFIGS, FALLBACK_CHAIN_CONFIG } from "config/chain"; import type { ChainConfig } from "config/chain"; -import { PROJECT_CONSTANTS } from "config/project"; +import { FALLBACK_CHAIN_CONFIG } from "config/chain"; import type { ProjectConstants } from "config/project"; +import { PROJECT_CONSTANTS } from "config/project"; import { FALLBACK_THEME, getTheme } from "config/theme"; import type { ThemeConfig } from "config/theme/types"; import { @@ -55,34 +57,38 @@ const DEFAULT_STATES: AppContextInterface = { const AppContext = createContext(DEFAULT_STATES); -export const AppProvider = ({ children }: AppProviderProps) => { +export const AppProvider = observer(({ children }: AppProviderProps) => { const { setModalTheme } = useModalTheme(); + const { chainConfigs, supportedChainIds } = useChainConfigs(); const [states, setStates] = useState(DEFAULT_STATES); - // Remark: this function is only used in useSelectChain. Do not use in other places. - const handleOnChainIdChange = useCallback((newChainId: string) => { - const chainConfig = CHAIN_CONFIGS[newChainId] ?? FALLBACK_CHAIN_CONFIG; - - const theme = getTheme(chainConfig.chain); - changeFavicon(theme.branding.favicon); - - setStates({ - isHydrated: true, - availableChainIds: SUPPORTED_CHAIN_IDS, - currentChainId: newChainId, - chainConfig, - indexerGraphClient: new GraphQLClient(chainConfig.indexer, { - headers: { - "x-hasura-admin-secret": HASURA_ADMIN_SECRET, - }, - }), - constants: PROJECT_CONSTANTS, - theme, - setTheme: (newTheme: ThemeConfig) => - setStates((prev) => ({ ...prev, theme: newTheme })), - }); - }, []); + // Remark: this function is only used in useNetworkChange. Do not use in other places. + const handleOnChainIdChange = useCallback( + (newChainId: string) => { + const chainConfig = chainConfigs[newChainId] ?? FALLBACK_CHAIN_CONFIG; + + const theme = getTheme(chainConfig.chain); + changeFavicon(theme.branding.favicon); + + setStates({ + isHydrated: true, + availableChainIds: supportedChainIds, + currentChainId: newChainId, + chainConfig, + indexerGraphClient: new GraphQLClient(chainConfig.indexer, { + headers: { + "x-hasura-admin-secret": HASURA_ADMIN_SECRET, + }, + }), + constants: PROJECT_CONSTANTS, + theme, + setTheme: (newTheme: ThemeConfig) => + setStates((prev) => ({ ...prev, theme: newTheme })), + }); + }, + [chainConfigs, supportedChainIds] + ); // Disable "Leave page" alert useEffect(() => { @@ -103,7 +109,7 @@ export const AppProvider = ({ children }: AppProviderProps) => { useNetworkChange(handleOnChainIdChange); return {children}; -}; +}); export const useCelatoneApp = (): AppContextInterface => { return useContext(AppContext); diff --git a/src/lib/app-provider/env.ts b/src/lib/app-provider/env.ts index 9bc3e4a57..06346f0f4 100644 --- a/src/lib/app-provider/env.ts +++ b/src/lib/app-provider/env.ts @@ -15,6 +15,7 @@ export enum CELATONE_QUERY_KEYS { ACCOUNT_DATA = "CELATONE_QUERY_ACCOUNT_DATA", ACCOUNT_TYPE = "CELATONE_QUERY_ACCOUNT_TYPE", ACCOUNT_TYPE_LCD = "CELATONE_QUERY_ACCOUNT_TYPE_LCD", + ACCOUNT_BECH_32_LCD = "CELATONE_QUERY_ACCOUNT_BECH_32_LCD", // ASSET ASSET_INFOS = "CELATONE_QUERY_ASSET_INFOS", // BLOCK @@ -99,6 +100,7 @@ export enum CELATONE_QUERY_KEYS { // TX TX_DATA = "CELATONE_QUERY_TX_DATA", TXS_BY_ADDRESS = "CELATONE_QUERY_TXS_BY_ADDRESS", + TXS_COUNT_SEQUENCER = "CELATONE_QUERY_TXS_COUNT_SEQUENCER", TXS_COUNT_BY_ADDRESS = "CELATONE_QUERY_TXS_COUNT_BY_ADDRESS", TXS_BY_ADDRESS_LCD = "CELATONE_QUERY_TXS_BY_ADDRESS_LCD", TXS_BY_ADDRESS_SEQUENCER = "CELATONE_QUERY_TXS_BY_ADDRESS_SEQUENCER", @@ -129,7 +131,6 @@ export enum CELATONE_QUERY_KEYS { MODULES_BY_ADDRESS_LCD = "CELATONE_QUERY_MODULES_BY_ADDRESS_LCD", MODULE_BY_ADDRESS_LCD = "CELATONE_QUERY_MODULE_BY_ADDRESS_LCD", MODULES = "CELATONE_QUERY_MODULES", - MODULE_VERIFICATION = "CELATONE_QUERY_MODULE_VERIFICATION", FUNCTION_VIEW = "CELATONE_QUERY_FUNCTION_VIEW", MODULE_DECODE = "CELATONE_QUERY_MODULE_DECODE", MODULE_TABLE_COUNTS = "CELATONE_QUERY_MODULE_TABLE_COUNTS", @@ -164,4 +165,6 @@ export enum CELATONE_QUERY_KEYS { NFTS_COUNT_BY_ACCOUNT = "CELATONE_QUERY_NFTS_COUNT_BY_ACCOUNT", NFTS_BY_ACCOUNT_BY_COLLECTION = "CELATONE_QUERY_NFTS_BY_ACCOUNT_BY_COLLECTION", NFTS_BY_ACCOUNT_BY_COLLECTION_SEQUENCER = "CELATONE_QUERY_NFTS_BY_ACCOUNT_BY_COLLECTION_SEQUENCER", + // VERIFICATION + MODULE_VERIFY_INFO = "CELATONE_QUERY_MODULE_VERIFY_INFO", } diff --git a/src/lib/app-provider/hooks/index.ts b/src/lib/app-provider/hooks/index.ts index 0cad469fb..7469d77b0 100644 --- a/src/lib/app-provider/hooks/index.ts +++ b/src/lib/app-provider/hooks/index.ts @@ -17,3 +17,5 @@ export * from "./useConvertHexAddress"; export * from "./usePlatform"; export * from "./useInitia"; export * from "./useIsConnected"; +export * from "./useChainConfigs"; +export * from "./useAllowCustomNetworks"; diff --git a/src/lib/app-provider/hooks/useAllowCustomNetworks.ts b/src/lib/app-provider/hooks/useAllowCustomNetworks.ts new file mode 100644 index 000000000..aed65b67d --- /dev/null +++ b/src/lib/app-provider/hooks/useAllowCustomNetworks.ts @@ -0,0 +1,19 @@ +import { useChainConfigs } from "./useChainConfigs"; +import { useInternalNavigate } from "./useInternalNavigate"; + +export const useAllowCustomNetworks = ({ + shouldRedirect, +}: { + shouldRedirect: boolean; +}) => { + const navigate = useInternalNavigate(); + const { supportedChainIds } = useChainConfigs(); + + const isAllow = supportedChainIds.some( + (chainId) => chainId === "initiation-1" + ); + + if (!isAllow && shouldRedirect) navigate({ pathname: "/", replace: true }); + + return isAllow; +}; diff --git a/src/lib/app-provider/hooks/useChainConfigs.ts b/src/lib/app-provider/hooks/useChainConfigs.ts new file mode 100644 index 000000000..b88a927f3 --- /dev/null +++ b/src/lib/app-provider/hooks/useChainConfigs.ts @@ -0,0 +1,168 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import type { ChainConfig as SharedChainConfig } from "@alleslabs/shared"; +import type { AssetList, Chain } from "@chain-registry/types"; +import { wallets as compassWallets } from "@cosmos-kit/compass"; +import { wallets as initiaWallets } from "@cosmos-kit/initia"; +import { wallets as keplrWallets } from "@cosmos-kit/keplr"; +import { wallets as staionWallets } from "@cosmos-kit/station"; +import { assets, chains } from "chain-registry"; +import { find } from "lodash"; +import { useCallback, useMemo } from "react"; + +import type { ChainConfig, ChainConfigs } from "config/chain"; +import { CHAIN_CONFIGS } from "config/chain"; +import { SUPPORTED_CHAIN_IDS } from "env"; +import { + initiatestnet, + initiatestnetAssets, +} from "lib/chain-registry/initiatestnet"; +import { + localosmosis, + localosmosisAsset, +} from "lib/chain-registry/localosmosis"; +import { sei, seiAssets } from "lib/chain-registry/sei"; +import { + terra2testnet, + terra2testnetAssets, +} from "lib/chain-registry/terra2testnet"; +import { useLocalChainConfigStore } from "lib/providers/store"; + +const getWallets = (wallets: SharedChainConfig["wallets"]) => + wallets.reduce( + (acc, wallet) => { + switch (wallet) { + case "keplr": + return [...acc, ...keplrWallets]; + case "initia": + return [...acc, ...initiaWallets]; + case "compass": + return [...acc, ...compassWallets]; + case "station": + return [...acc, ...staionWallets]; + default: + return acc; + } + }, + [] as ChainConfig["wallets"] + ); + +export const useChainConfigs = (): { + chainConfigs: ChainConfigs; + registryChains: Chain[]; + registryAssets: AssetList[]; + supportedChainIds: string[]; + isChainIdExist: (chainId: string) => boolean; + isPrettyNameExist: (name: string) => boolean; +} => { + const { localChainConfigs, isLocalChainIdExist, isLocalPrettyNameExist } = + useLocalChainConfigStore(); + + const local = useMemo( + () => + Object.values(localChainConfigs).reduce( + (acc, each) => { + const localChainConfig: ChainConfig = { + tier: each.tier, + chain: each.chain, + registryChainName: each.registryChainName, + prettyName: each.prettyName, + logoUrl: + each.logo_URIs?.png ?? + each.logo_URIs?.svg ?? + each.logo_URIs?.jpeg, + networkType: each.network_type, + lcd: each.lcd, + rpc: each.rpc, + indexer: each.graphql || "", + wallets: getWallets(each.wallets), + features: each.features, + gas: { + gasPrice: { + tokenPerGas: each.fees?.fee_tokens[0]?.fixed_min_gas_price ?? 0, + denom: each.fees?.fee_tokens[0]?.denom ?? "", + }, + gasAdjustment: each.gas.gasAdjustment, + maxGasLimit: each.gas.maxGasLimit, + }, + extra: each.extra, + }; + + const localRegistryChain: Chain = { + chain_name: each.registryChainName, + status: "live", + network_type: each.network_type, + pretty_name: each.prettyName, + chain_id: each.chainId, + bech32_prefix: each.registry?.bech32_prefix ?? "", + slip44: each.registry?.slip44 ?? 118, + fees: each.fees, + staking: each.registry?.staking, + logo_URIs: each.logo_URIs, + }; + + const localRegistryAssets: AssetList = { + $schema: "../assetlist.schema.json", + chain_name: each.registryChainName, + assets: each.registry?.assets ?? [], + }; + + return { + chainConfigs: { + ...acc.chainConfigs, + [each.chainId]: localChainConfig, + }, + registryChains: [...acc.registryChains, localRegistryChain], + registryAssets: [...acc.registryAssets, localRegistryAssets], + supportedChainIds: [...acc.supportedChainIds, each.chainId], + }; + }, + { + chainConfigs: {} as ChainConfigs, + registryChains: [] as Chain[], + registryAssets: [] as AssetList[], + supportedChainIds: [] as string[], + } + ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(localChainConfigs)] + ); + + const isChainIdExist = useCallback( + (chainId: string) => + !!CHAIN_CONFIGS[chainId] || isLocalChainIdExist(chainId), + [isLocalChainIdExist] + ); + + const isPrettyNameExist = useCallback( + (name: string) => + !!find(CHAIN_CONFIGS, { prettyName: name }) || + isLocalPrettyNameExist(name), + [isLocalPrettyNameExist] + ); + + return { + chainConfigs: { + ...CHAIN_CONFIGS, + ...local.chainConfigs, + }, + registryChains: [ + ...chains, + localosmosis, + sei, + terra2testnet, + ...initiatestnet, + ...local.registryChains, + ], + registryAssets: [ + ...assets, + localosmosisAsset, + seiAssets, + terra2testnetAssets, + ...initiatestnetAssets, + ...local.registryAssets, + ], + supportedChainIds: [...SUPPORTED_CHAIN_IDS, ...local.supportedChainIds], + isChainIdExist, + isPrettyNameExist, + }; +}; diff --git a/src/lib/app-provider/hooks/useInternalNavigate.ts b/src/lib/app-provider/hooks/useInternalNavigate.ts index a84fdaf2f..471048bca 100644 --- a/src/lib/app-provider/hooks/useInternalNavigate.ts +++ b/src/lib/app-provider/hooks/useInternalNavigate.ts @@ -3,9 +3,11 @@ import type { Router } from "next/router"; import type { ParsedUrlQueryInput } from "node:querystring"; import { useCallback } from "react"; -import { FALLBACK_SUPPORTED_CHAIN_ID, SUPPORTED_CHAIN_IDS } from "env"; +import { FALLBACK_SUPPORTED_CHAIN_ID } from "env"; import { getFirstQueryParam } from "lib/utils"; +import { useChainConfigs } from "./useChainConfigs"; + export interface NavigationArgs { pathname: string; query?: ParsedUrlQueryInput; @@ -15,6 +17,7 @@ export interface NavigationArgs { export const useInternalNavigate = () => { const router = useRouter(); + const { supportedChainIds } = useChainConfigs(); return useCallback( ({ @@ -28,7 +31,7 @@ export const useInternalNavigate = () => { { pathname: `/[network]${pathname}`, query: { - network: SUPPORTED_CHAIN_IDS.includes( + network: supportedChainIds.includes( getFirstQueryParam(router.query.network) ) ? router.query.network @@ -40,6 +43,6 @@ export const useInternalNavigate = () => { options ); }, - [router] + [router.push, router.query.network, router.replace, supportedChainIds] ); }; diff --git a/src/lib/app-provider/hooks/useNetworkChange.ts b/src/lib/app-provider/hooks/useNetworkChange.ts index 03a9631da..fccb404cb 100644 --- a/src/lib/app-provider/hooks/useNetworkChange.ts +++ b/src/lib/app-provider/hooks/useNetworkChange.ts @@ -1,9 +1,11 @@ import { useRouter } from "next/router"; import { useEffect, useRef } from "react"; -import { FALLBACK_SUPPORTED_CHAIN_ID, SUPPORTED_CHAIN_IDS } from "env"; +import { FALLBACK_SUPPORTED_CHAIN_ID } from "env"; +import { useLocalChainConfigStore } from "lib/providers/store"; import { getFirstQueryParam } from "lib/utils"; +import { useChainConfigs } from "./useChainConfigs"; import { useInternalNavigate } from "./useInternalNavigate"; export const useNetworkChange = ( @@ -12,9 +14,11 @@ export const useNetworkChange = ( const router = useRouter(); const networkRef = useRef(); const navigate = useInternalNavigate(); + const { supportedChainIds } = useChainConfigs(); + const { isHydrated: isChainConfigStoreHydrated } = useLocalChainConfigStore(); useEffect(() => { - if (router.isReady) { + if (router.isReady && isChainConfigStoreHydrated) { const networkRoute = getFirstQueryParam(router.query.network); // Redirect to default chain if there is no network query provided if (!router.query.network) { @@ -25,7 +29,7 @@ export const useNetworkChange = ( }); } else if ( router.pathname === "/[network]" && - !SUPPORTED_CHAIN_IDS.includes(networkRoute) + !supportedChainIds.includes(networkRoute) ) { // Redirect to default network 404 if `/invalid_network` navigate({ @@ -45,7 +49,7 @@ export const useNetworkChange = ( navigate({ pathname: "/404", query: { - network: SUPPORTED_CHAIN_IDS.includes(networkRoute) + network: supportedChainIds.includes(networkRoute) ? networkRoute : FALLBACK_SUPPORTED_CHAIN_ID, }, @@ -53,10 +57,12 @@ export const useNetworkChange = ( } }, [ handleOnChainIdChange, + isChainConfigStoreHydrated, navigate, router.asPath, router.isReady, router.pathname, router.query, + supportedChainIds, ]); }; diff --git a/src/lib/app-provider/hooks/useRestrictedInput.ts b/src/lib/app-provider/hooks/useRestrictedInput.ts index 30c826086..182a21936 100644 --- a/src/lib/app-provider/hooks/useRestrictedInput.ts +++ b/src/lib/app-provider/hooks/useRestrictedInput.ts @@ -46,15 +46,15 @@ export function useRestrictedInput( export interface RestrictedNumberInputParams { type?: "decimal" | "integer"; maxDecimalPoints?: number; - maxIntegerPoinsts?: number; + maxIntegerPoints?: number; onChange?: (event: ChangeEvent) => void; } // eslint-disable-next-line sonarjs/cognitive-complexity export function useRestrictedNumberInput({ type = "decimal", - maxDecimalPoints, - maxIntegerPoinsts, + maxDecimalPoints = 6, + maxIntegerPoints = 7, onChange: _onChange, }: RestrictedNumberInputParams): RestrictedInputReturn { const { onKeyPress: restrictCharacters } = useRestrictedInput( @@ -65,14 +65,14 @@ export function useRestrictedNumberInput({ (nextValue: string): boolean => { return ( Number.isNaN(+nextValue) || - (typeof maxIntegerPoinsts === "number" && - new RegExp(`^[0-9]{${maxIntegerPoinsts + 1},}`).test(nextValue)) || + (typeof maxIntegerPoints === "number" && + new RegExp(`^[0-9]{${maxIntegerPoints + 1},}`).test(nextValue)) || (type === "decimal" && typeof maxDecimalPoints === "number" && new RegExp(`\\.[0-9]{${maxDecimalPoints + 1},}$`).test(nextValue)) ); }, - [maxDecimalPoints, maxIntegerPoinsts, type] + [maxDecimalPoints, maxIntegerPoints, type] ); const onKeyPress = useCallback( diff --git a/src/lib/components/WasmPageContainer.tsx b/src/lib/components/ActionPageContainer.tsx similarity index 63% rename from src/lib/components/WasmPageContainer.tsx rename to src/lib/components/ActionPageContainer.tsx index 6637b6379..23f9d25be 100644 --- a/src/lib/components/WasmPageContainer.tsx +++ b/src/lib/components/ActionPageContainer.tsx @@ -2,17 +2,22 @@ import type { BoxProps } from "@chakra-ui/react"; import { Flex } from "@chakra-ui/react"; import type { ReactNode } from "react"; -type WasmPageContainerProps = { +type ActionPageContainerProps = { children: ReactNode; boxProps?: BoxProps; + width?: number; }; -const WasmPageContainer = ({ children, boxProps }: WasmPageContainerProps) => ( +const ActionPageContainer = ({ + children, + boxProps, + width = 540, +}: ActionPageContainerProps) => ( ( ); -export default WasmPageContainer; +export default ActionPageContainer; diff --git a/src/lib/components/AppLink.tsx b/src/lib/components/AppLink.tsx index 21ebcfbbe..09fdc7712 100644 --- a/src/lib/components/AppLink.tsx +++ b/src/lib/components/AppLink.tsx @@ -2,7 +2,8 @@ import { Text } from "@chakra-ui/react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { FALLBACK_SUPPORTED_CHAIN_ID, SUPPORTED_CHAIN_IDS } from "env"; +import { FALLBACK_SUPPORTED_CHAIN_ID } from "env"; +import { useChainConfigs } from "lib/app-provider/hooks/useChainConfigs"; import { getFirstQueryParam } from "lib/utils"; export const AppLink = ({ @@ -10,9 +11,10 @@ export const AppLink = ({ ...linkProps }: React.ComponentProps) => { const router = useRouter(); + const { supportedChainIds } = useChainConfigs(); const componentHref = linkProps.href.toString(); - const network = SUPPORTED_CHAIN_IDS.includes( + const network = supportedChainIds.includes( getFirstQueryParam(router.query.network) ) ? router.query.network diff --git a/src/lib/components/ButtonCard.tsx b/src/lib/components/ButtonCard.tsx index 644bd9e5d..56a541619 100644 --- a/src/lib/components/ButtonCard.tsx +++ b/src/lib/components/ButtonCard.tsx @@ -1,5 +1,5 @@ import type { FlexProps } from "@chakra-ui/react"; -import { Flex, Heading, Stack, Text } from "@chakra-ui/react"; +import { Flex, Heading, Stack, Tag, Text } from "@chakra-ui/react"; import type { ReactNode } from "react"; import { CustomIcon } from "./icon"; @@ -9,6 +9,8 @@ interface ButtonCardProps extends FlexProps { description: ReactNode; onClick: () => void; disabled?: boolean; + tagLabel?: string; + hasIcon?: boolean; } export const ButtonCard = ({ @@ -16,6 +18,8 @@ export const ButtonCard = ({ description, onClick, disabled, + tagLabel, + hasIcon = true, ...componentProps }: ButtonCardProps) => ( - - {title} - + + + {title} + + {tagLabel && {tagLabel}} + {typeof description === "string" ? ( {description} @@ -52,6 +59,6 @@ export const ButtonCard = ({ description )} - + {hasIcon && } ); diff --git a/src/lib/components/MobileGuard.tsx b/src/lib/components/MobileGuard.tsx index 901b5d7c6..451f0b38d 100644 --- a/src/lib/components/MobileGuard.tsx +++ b/src/lib/components/MobileGuard.tsx @@ -1,8 +1,7 @@ import { useRouter } from "next/router"; import type { ReactNode } from "react"; -import { SUPPORTED_CHAIN_IDS } from "env"; -import { useMobile } from "lib/app-provider"; +import { useChainConfigs, useMobile } from "lib/app-provider"; import { NoMobile } from "./modal"; @@ -11,11 +10,12 @@ interface MobileGuardProps { } export const MobileGuard = ({ children }: MobileGuardProps) => { const router = useRouter(); + const { supportedChainIds } = useChainConfigs(); const isMobile = useMobile(); const pathName = router.asPath; const isResponsive = - SUPPORTED_CHAIN_IDS.includes(pathName.slice(1)) || + supportedChainIds.includes(pathName.slice(1)) || pathName.includes(`/accounts`) || pathName.includes(`/txs`) || pathName.includes(`/blocks`) || diff --git a/src/lib/components/custom-network/CustomNetworkPageHeader.tsx b/src/lib/components/custom-network/CustomNetworkPageHeader.tsx new file mode 100644 index 000000000..9cef3ad56 --- /dev/null +++ b/src/lib/components/custom-network/CustomNetworkPageHeader.tsx @@ -0,0 +1,34 @@ +import { Alert, AlertDescription, Flex, Heading, Text } from "@chakra-ui/react"; + +interface CustomNetworkPageHeaderProps { + title: string; + subtitle?: string; + hasAlert?: boolean; +} + +export const CustomNetworkPageHeader = ({ + title, + subtitle = "Add Custom Minitia", + hasAlert = true, +}: CustomNetworkPageHeaderProps) => ( + <> + + + {subtitle} + + + {title} + + + {hasAlert && ( + + + + Please note that the custom Minitia you add on our website will only + be stored locally on your device. + + + + )} + +); diff --git a/src/lib/components/custom-network/CustomNetworkSubheader.tsx b/src/lib/components/custom-network/CustomNetworkSubheader.tsx new file mode 100644 index 000000000..7496f5507 --- /dev/null +++ b/src/lib/components/custom-network/CustomNetworkSubheader.tsx @@ -0,0 +1,24 @@ +import { Flex, Heading, Text } from "@chakra-ui/react"; + +interface CustomNetworkSubheaderProps { + title: string; + subtitle?: string; +} + +export const CustomNetworkSubheader = ({ + title, + subtitle, +}: CustomNetworkSubheaderProps) => { + return ( + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + ); +}; diff --git a/src/lib/components/custom-network/index.ts b/src/lib/components/custom-network/index.ts new file mode 100644 index 000000000..c9d8e41ef --- /dev/null +++ b/src/lib/components/custom-network/index.ts @@ -0,0 +1,2 @@ +export * from "./CustomNetworkSubheader"; +export * from "./CustomNetworkPageHeader"; diff --git a/src/lib/components/forms/ControllerInput.tsx b/src/lib/components/forms/ControllerInput.tsx index cc977bdc9..fc918e0fc 100644 --- a/src/lib/components/forms/ControllerInput.tsx +++ b/src/lib/components/forms/ControllerInput.tsx @@ -18,6 +18,7 @@ import type { } from "react-hook-form"; import { useController, useWatch } from "react-hook-form"; +import type { RestrictedNumberInputParams } from "lib/app-provider"; import { useRestrictedNumberInput } from "lib/app-provider"; import type { FormStatus } from "./FormStatus"; @@ -33,10 +34,15 @@ export interface ControllerInputProps status?: FormStatus; maxLength?: number; helperAction?: ReactNode; + textAlign?: "left" | "right"; cta?: { label: string; onClick: (changeValue?: (...event: string[]) => void) => void; }; + restrictedNumberInputParams?: Pick< + RestrictedNumberInputParams, + "maxDecimalPoints" | "maxIntegerPoints" + >; } export const ControllerInput = ({ @@ -55,7 +61,9 @@ export const ControllerInput = ({ autoFocus, cursor, helperAction, + textAlign = "left", cta, + restrictedNumberInputParams, ...componentProps }: ControllerInputProps) => { const watcher = useWatch({ @@ -83,19 +91,22 @@ export const ControllerInput = ({ return "3rem"; } + if (textAlign === "right") { + return "1rem"; + } + return 0; }; const decimalHandlers = useRestrictedNumberInput({ type: "decimal", - maxIntegerPoinsts: 7, - maxDecimalPoints: 6, onChange: field.onChange, + ...restrictedNumberInputParams, }); const numberHandlers = useRestrictedNumberInput({ type: "integer", - maxIntegerPoinsts: 7, + maxIntegerPoints: 7, maxDecimalPoints: 0, onChange: field.onChange, }); @@ -130,11 +141,18 @@ export const ControllerInput = ({ maxLength={maxLength} autoFocus={autoFocus} cursor={cursor} + onBlur={field.onBlur} + onChange={field.onChange} pr={inputPaddingRight()} {...(type === "decimal" && decimalHandlers)} {...(type === "number" && numberHandlers)} + textAlign={textAlign} /> - + {status && getStatusIcon(status.state)} {cta && ( + + + ), + viewBox: "0 0 24 24", + }, }; export type IconKeys = keyof typeof ICONS; diff --git a/src/lib/components/layouts/FooterCta.tsx b/src/lib/components/layouts/FooterCta.tsx index f7baa8606..20ce2cd1d 100644 --- a/src/lib/components/layouts/FooterCta.tsx +++ b/src/lib/components/layouts/FooterCta.tsx @@ -1,5 +1,5 @@ -import type { ButtonProps } from "@chakra-ui/react"; -import { Box, Button, Flex, Spinner } from "@chakra-ui/react"; +import type { ButtonProps, SystemStyleObject } from "@chakra-ui/react"; +import { Button, Flex, Spinner, Text } from "@chakra-ui/react"; interface FooterCtaProps { loading?: boolean; @@ -7,6 +7,8 @@ interface FooterCtaProps { cancelLabel?: string; actionButton: ButtonProps; actionLabel?: string; + helperText?: string; + sx?: SystemStyleObject; } export const FooterCta = ({ @@ -15,23 +17,35 @@ export const FooterCta = ({ cancelLabel = "Previous", actionButton, actionLabel = "Submit", + helperText, + sx, }: FooterCtaProps) => ( - - - + {helperText && ( + + {helperText} + + )} + ); diff --git a/src/lib/components/modal/RemoveChainConfigModal.tsx b/src/lib/components/modal/RemoveChainConfigModal.tsx new file mode 100644 index 000000000..18eba56d0 --- /dev/null +++ b/src/lib/components/modal/RemoveChainConfigModal.tsx @@ -0,0 +1,69 @@ +import { Text, useToast } from "@chakra-ui/react"; +import { useRouter } from "next/router"; +import type { ReactNode } from "react"; + +import { useCelatoneApp } from "lib/app-provider"; +import { CustomIcon } from "lib/components/icon"; +import { useLocalChainConfigStore, useNetworkStore } from "lib/providers/store"; + +import { ActionModal } from "./ActionModal"; + +interface RemoveChainConfigModalProps { + chainId: string; + trigger: ReactNode; +} + +export function RemoveChainConfigModal({ + chainId, + trigger, +}: RemoveChainConfigModalProps) { + const { removeLocalChainConfig, getLocalChainConfig } = + useLocalChainConfigStore(); + const { removeNetwork } = useNetworkStore(); + const router = useRouter(); + const { currentChainId } = useCelatoneApp(); + + const chainConfig = getLocalChainConfig(chainId); + + const toast = useToast(); + const handleRemove = () => { + // remove chain id from chain config store + removeLocalChainConfig(chainId); + + // remove chain id from pinned network + removeNetwork(chainId); + + // redirect to home page + router.push(currentChainId === chainId ? "/" : `/${currentChainId}`); + + setTimeout(() => { + toast({ + title: `Removed '${chainConfig?.prettyName}'`, + status: "success", + duration: 5000, + isClosable: false, + position: "bottom-right", + icon: , + }); + }, 1000); + }; + + return ( + + + All information about your Minitia will be lost and can‘t be + recovered. You can save this address again later, but you will need to + add its new address name. + + + ); +} diff --git a/src/lib/components/module/ModuleCard.tsx b/src/lib/components/module/ModuleCard.tsx index 2de5cd00a..74d47e58e 100644 --- a/src/lib/components/module/ModuleCard.tsx +++ b/src/lib/components/module/ModuleCard.tsx @@ -6,7 +6,7 @@ import { AppLink } from "../AppLink"; import { CustomIcon } from "../icon"; import { AmpEvent, track } from "lib/amplitude"; import { ModuleInteractionMobileStep } from "lib/pages/interact/types"; -import { useVerifyModule } from "lib/services/move/module"; +import { useMoveVerifyInfo } from "lib/services/move/module"; import type { BechAddr, IndexedModule, Option } from "lib/types"; import { CountBadge } from "./CountBadge"; @@ -28,7 +28,7 @@ export const ModuleCard = ({ setStep, readOnly = false, }: ModuleCardProps) => { - const { data: isVerified } = useVerifyModule({ + const { data: isVerified } = useMoveVerifyInfo({ address: selectedAddress, moduleName: module.moduleName, }); diff --git a/src/lib/layout/network-menu/NetworkAccordion.tsx b/src/lib/layout/network-menu/NetworkAccordion.tsx index 97adc66ad..63ae77286 100644 --- a/src/lib/layout/network-menu/NetworkAccordion.tsx +++ b/src/lib/layout/network-menu/NetworkAccordion.tsx @@ -6,8 +6,9 @@ import { Flex, Heading, } from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; -import { CHAIN_CONFIGS } from "config/chain"; +import { useChainConfigs } from "lib/app-provider"; import type { Option } from "lib/types"; import { NetworkAccordionSubsection } from "./NetworkAccordionSubsection"; @@ -21,69 +22,74 @@ interface NetworkAccordionProps { onClose: () => void; } -export const NetworkAccordion = ({ - title, - networks, - cursor, - setCursor, - startIndex, - onClose, -}: NetworkAccordionProps) => { - const nonInitiaNetworks = networks.filter( - (chainId) => CHAIN_CONFIGS[chainId]?.extra.layer === undefined - ); - const l1Networks = networks.filter( - (chainId) => CHAIN_CONFIGS[chainId]?.extra.layer === "1" - ); - const l2Networks = networks.filter( - (chainId) => CHAIN_CONFIGS[chainId]?.extra.layer === "2" - ); +export const NetworkAccordion = observer( + ({ + title, + networks, + cursor, + setCursor, + startIndex, + onClose, + }: NetworkAccordionProps) => { + const { chainConfigs } = useChainConfigs(); + const nonInitiaNetworks = networks.filter( + (chainId) => chainConfigs[chainId]?.extra.layer === undefined + ); + const l1Networks = networks.filter( + (chainId) => chainConfigs[chainId]?.extra.layer === "1" + ); + const l2Networks = networks.filter( + (chainId) => chainConfigs[chainId]?.extra.layer === "2" + ); - return ( - - ); -}; + return ( + + ); + } +); diff --git a/src/lib/layout/network-menu/NetworkAccordionLocal.tsx b/src/lib/layout/network-menu/NetworkAccordionLocal.tsx new file mode 100644 index 000000000..fd571a361 --- /dev/null +++ b/src/lib/layout/network-menu/NetworkAccordionLocal.tsx @@ -0,0 +1,55 @@ +import { + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Flex, + Heading, +} from "@chakra-ui/react"; + +import type { Option } from "lib/types"; + +import { NetworkCard } from "./network-card"; + +interface NetworkAccordionLocalProps { + networks: string[]; + cursor: Option; + setCursor: (index: Option) => void; + startIndex: number; + onClose: () => void; +} + +export const NetworkAccordionLocal = ({ + networks, + cursor, + setCursor, + startIndex, + onClose, +}: NetworkAccordionLocalProps) => ( + +); diff --git a/src/lib/layout/network-menu/NetworkAccordionPinned.tsx b/src/lib/layout/network-menu/NetworkAccordionPinned.tsx index b7b45b7d5..9c80651e8 100644 --- a/src/lib/layout/network-menu/NetworkAccordionPinned.tsx +++ b/src/lib/layout/network-menu/NetworkAccordionPinned.tsx @@ -22,6 +22,7 @@ import { SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; +import { observer } from "mobx-react-lite"; import { useMemo, useState } from "react"; import { createPortal } from "react-dom"; @@ -37,114 +38,116 @@ interface NetworkAccodionPinnedProps { onClose: () => void; } -export const NetworkAccodionPinned = ({ - pinnedNetworks, - cursor, - setCursor, - onClose, -}: NetworkAccodionPinnedProps) => { - const { getPinnedNetworks, setPinnedNetworks } = useNetworkStore(); - const [dndActive, setDndActive] = useState>(null); +export const NetworkAccodionPinned = observer( + ({ + pinnedNetworks, + cursor, + setCursor, + onClose, + }: NetworkAccodionPinnedProps) => { + const { getPinnedNetworks, setPinnedNetworks } = useNetworkStore(); + const [dndActive, setDndActive] = useState>(null); - // Drag and drop feature - const activeItem = useMemo( - () => pinnedNetworks.find((item) => item === dndActive?.id), - [dndActive, pinnedNetworks] - ); + // Drag and drop feature + const activeItem = useMemo( + () => pinnedNetworks.find((item) => item === dndActive?.id), + [dndActive, pinnedNetworks] + ); - const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - delay: 100, - tolerance: 5, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 100, - tolerance: 5, - }, - }) - ); + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + delay: 100, + tolerance: 5, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 100, + tolerance: 5, + }, + }) + ); - return ( - - ); -}; + ))} + + {createPortal( + + {activeItem ? ( + + ) : null} + , + document.body + )} + + + + + ); + } +); diff --git a/src/lib/layout/network-menu/NetworkImage.tsx b/src/lib/layout/network-menu/NetworkImage.tsx index fa7c47c19..b60959abf 100644 --- a/src/lib/layout/network-menu/NetworkImage.tsx +++ b/src/lib/layout/network-menu/NetworkImage.tsx @@ -1,23 +1,25 @@ import { Image, useToken } from "@chakra-ui/react"; -import { CHAIN_CONFIGS } from "config/chain"; +import { useChainConfigs } from "lib/app-provider"; interface NetworkImageProps { chainId: string; } export const NetworkImage = ({ chainId }: NetworkImageProps) => { + const { chainConfigs } = useChainConfigs(); const [secondaryDarker] = useToken("colors", ["secondary.darker"]); - const image = CHAIN_CONFIGS[chainId]?.logoUrl; - const fallbackImage = `https://ui-avatars.com/api/?name=${CHAIN_CONFIGS[chainId]?.prettyName || chainId}&background=${secondaryDarker.replace("#", "")}&color=fff`; + const image = chainConfigs[chainId]?.logoUrl; + const fallbackImage = `https://ui-avatars.com/api/?name=${chainConfigs[chainId]?.prettyName || chainId}&background=${secondaryDarker.replace("#", "")}&color=fff`; return ( diff --git a/src/lib/layout/network-menu/NetworkMenuBody.tsx b/src/lib/layout/network-menu/NetworkMenuBody.tsx index db8c49c9a..75e4989a7 100644 --- a/src/lib/layout/network-menu/NetworkMenuBody.tsx +++ b/src/lib/layout/network-menu/NetworkMenuBody.tsx @@ -1,10 +1,14 @@ -import { Accordion, Divider, Flex } from "@chakra-ui/react"; +import { Accordion, Button, Divider, Flex } from "@chakra-ui/react"; import { observer } from "mobx-react-lite"; +import { useAllowCustomNetworks } from "lib/app-provider"; +import { AppLink } from "lib/components/AppLink"; +import { CustomIcon } from "lib/components/icon"; import { EmptyState } from "lib/components/state"; import type { Option } from "lib/types"; import { NetworkAccordion } from "./NetworkAccordion"; +import { NetworkAccordionLocal } from "./NetworkAccordionLocal"; import { NetworkAccodionPinned } from "./NetworkAccordionPinned"; interface NetworkMenuBodyProps { @@ -13,6 +17,7 @@ interface NetworkMenuBodyProps { filteredPinnedChains: string[]; filteredMainnetChains: string[]; filteredTestnetChains: string[]; + filteredLocalChains: string[]; onClose: () => void; } @@ -23,57 +28,99 @@ export const NetworkMenuBody = observer( filteredPinnedChains, filteredMainnetChains, filteredTestnetChains, + filteredLocalChains, onClose, - }: NetworkMenuBodyProps) => ( - <> - - - - {!!filteredPinnedChains.length && } - - {!!filteredMainnetChains.length && } - - - - {filteredPinnedChains.length + - filteredMainnetChains.length + - filteredTestnetChains.length === - 0 && ( - + + + {!!filteredPinnedChains.length && ( + + )} + + {!!filteredMainnetChains.length && ( + + )} + + {isAllowCustomNetworks && ( + <> + + + + + + + )} + + + {filteredPinnedChains.length + + filteredMainnetChains.length + + filteredTestnetChains.length + + filteredLocalChains.length === + 0 && ( + - )} - - ) + /> + )} + + ); + } ); diff --git a/src/lib/layout/network-menu/NetworkMenuTop.tsx b/src/lib/layout/network-menu/NetworkMenuTop.tsx index ec4ccb3d0..6ef52138e 100644 --- a/src/lib/layout/network-menu/NetworkMenuTop.tsx +++ b/src/lib/layout/network-menu/NetworkMenuTop.tsx @@ -1,55 +1,76 @@ -import { Flex, Heading, Kbd, Text } from "@chakra-ui/react"; +import { Box, Flex, Heading, Kbd, Text } from "@chakra-ui/react"; +import { observer } from "mobx-react-lite"; import type { KeyboardEvent as ReactKeyboardEvent } from "react"; -import { useIsMac, useMobile } from "lib/app-provider"; +import { useAllowCustomNetworks, useIsMac, useMobile } from "lib/app-provider"; +import { AppLink } from "lib/components/AppLink"; import InputWithIcon from "lib/components/InputWithIcon"; interface NetworkMenuTopProps { keyword: string; setKeyword: (value: string) => void; handleOnKeyDown: (e: ReactKeyboardEvent) => void; + onClose: () => void; } -export const NetworkMenuTop = ({ - keyword, - setKeyword, - handleOnKeyDown, -}: NetworkMenuTopProps) => { - const isMobile = useMobile(); - const isMac = useIsMac(); +export const NetworkMenuTop = observer( + ({ keyword, setKeyword, handleOnKeyDown, onClose }: NetworkMenuTopProps) => { + const isMobile = useMobile(); + const isMac = useIsMac(); + const isAllowCustomNetworks = useAllowCustomNetworks({ + shouldRedirect: false, + }); - return ( - - - - - Select Network - - {!isMobile && ( - - - - {isMac ? "⌘" : "Ctrl"} + return ( + + + + + Select Network + + {!isMobile && ( + + + + {isMac ? "⌘" : "Ctrl"} + + + + + / + + + + )} + + {isAllowCustomNetworks && ( + + + Want to add your network? + {" "} + + + Add a custom chain. - - - - / - - - + + )} + setKeyword(e.target.value)} + onKeyDown={handleOnKeyDown} + amptrackSection="network-search" + /> - setKeyword(e.target.value)} - onKeyDown={handleOnKeyDown} - amptrackSection="network-search" - /> - - ); -}; + ); + } +); diff --git a/src/lib/layout/network-menu/hooks/useNetworkSelector.ts b/src/lib/layout/network-menu/hooks/useNetworkSelector.ts index 06c64b5b3..9c54d3a46 100644 --- a/src/lib/layout/network-menu/hooks/useNetworkSelector.ts +++ b/src/lib/layout/network-menu/hooks/useNetworkSelector.ts @@ -2,12 +2,13 @@ import { isUndefined } from "lodash"; import type { KeyboardEvent as ReactKeyboardEvent } from "react"; import { useCallback, useMemo, useState } from "react"; -import { useCelatoneApp } from "lib/app-provider"; +import { useCelatoneApp, useChainConfigs } from "lib/app-provider"; import { useNetworkStore } from "lib/providers/store"; import { filterChains, getNextCursor } from "./utils"; export const useNetworkSelector = (onClose: () => void) => { + const { chainConfigs } = useChainConfigs(); const { availableChainIds } = useCelatoneApp(); const { getPinnedNetworks } = useNetworkStore(); @@ -15,20 +16,26 @@ export const useNetworkSelector = (onClose: () => void) => { const [cursor, setCursor] = useState(); const pinnedNetworks = getPinnedNetworks(); - const [filteredPinnedChains, filteredMainnetChains, filteredTestnetChains] = - useMemo( - () => [ - filterChains(pinnedNetworks, keyword), - filterChains(availableChainIds, keyword, "mainnet"), - filterChains(availableChainIds, keyword, "testnet"), - ], - [availableChainIds, keyword, pinnedNetworks] - ); + const [ + filteredPinnedChains, + filteredMainnetChains, + filteredTestnetChains, + filteredLocalChains, + ] = useMemo( + () => [ + filterChains(chainConfigs, pinnedNetworks, keyword), + filterChains(chainConfigs, availableChainIds, keyword, "mainnet"), + filterChains(chainConfigs, availableChainIds, keyword, "testnet"), + filterChains(chainConfigs, availableChainIds, keyword, "local"), + ], + [availableChainIds, chainConfigs, keyword, pinnedNetworks] + ); const totalNetworks = filteredPinnedChains.length + filteredMainnetChains.length + - filteredTestnetChains.length; + filteredTestnetChains.length + + filteredLocalChains.length; const handleOnKeyDown = useCallback( (e: ReactKeyboardEvent) => { @@ -67,5 +74,6 @@ export const useNetworkSelector = (onClose: () => void) => { filteredPinnedChains, filteredMainnetChains, filteredTestnetChains, + filteredLocalChains, }; }; diff --git a/src/lib/layout/network-menu/hooks/utils.ts b/src/lib/layout/network-menu/hooks/utils.ts index 90155466c..c08843959 100644 --- a/src/lib/layout/network-menu/hooks/utils.ts +++ b/src/lib/layout/network-menu/hooks/utils.ts @@ -1,20 +1,20 @@ -import type { ChainConfig } from "config/chain"; -import { CHAIN_CONFIGS } from "config/chain"; +import type { ChainConfig, ChainConfigs } from "config/chain"; import type { Option } from "lib/types"; export const filterChains = ( + chainConfigs: ChainConfigs, chainIds: string[], keyword: string, type?: ChainConfig["networkType"] ) => { const chainIdsByType = type - ? chainIds.filter((chainId) => CHAIN_CONFIGS[chainId]?.networkType === type) + ? chainIds.filter((chainId) => chainConfigs[chainId]?.networkType === type) : chainIds; return chainIdsByType.filter( (chainId) => !keyword || - CHAIN_CONFIGS[chainId]?.prettyName + chainConfigs[chainId]?.prettyName .toLowerCase() .includes(keyword.toLowerCase()) || chainId.toLowerCase().includes(keyword.toLowerCase()) diff --git a/src/lib/layout/network-menu/index.tsx b/src/lib/layout/network-menu/index.tsx index cf338519d..5f4c3df85 100644 --- a/src/lib/layout/network-menu/index.tsx +++ b/src/lib/layout/network-menu/index.tsx @@ -31,6 +31,7 @@ export const NetworkMenu = observer(() => { filteredPinnedChains, filteredMainnetChains, filteredTestnetChains, + filteredLocalChains, } = useNetworkSelector(onClose); useNetworkShortCut(onToggle); @@ -62,6 +63,7 @@ export const NetworkMenu = observer(() => { keyword={keyword} setKeyword={setKeyword} handleOnKeyDown={handleOnKeyDown} + onClose={onClose} /> @@ -72,6 +74,7 @@ export const NetworkMenu = observer(() => { filteredPinnedChains={filteredPinnedChains} filteredMainnetChains={filteredMainnetChains} filteredTestnetChains={filteredTestnetChains} + filteredLocalChains={filteredLocalChains} onClose={onClose} /> diff --git a/src/lib/layout/network-menu/network-card/NetworkCard.tsx b/src/lib/layout/network-menu/network-card/NetworkCard.tsx index 5bfceba65..4bc018949 100644 --- a/src/lib/layout/network-menu/network-card/NetworkCard.tsx +++ b/src/lib/layout/network-menu/network-card/NetworkCard.tsx @@ -3,8 +3,12 @@ import { observer } from "mobx-react-lite"; import { useCallback } from "react"; import { NetworkImage } from "../NetworkImage"; -import { CHAIN_CONFIGS } from "config/chain"; -import { useCelatoneApp, useMobile, useSelectChain } from "lib/app-provider"; +import { + useCelatoneApp, + useChainConfigs, + useMobile, + useSelectChain, +} from "lib/app-provider"; import type { Option } from "lib/types"; import { NetworkCardCta } from "./NetworkCardCta"; @@ -51,6 +55,7 @@ export const NetworkCard = observer( onClose, isDraggable = false, }: NetworkCardProps) => { + const { chainConfigs } = useChainConfigs(); const isMobile = useMobile(); const { currentChainId } = useCelatoneApp(); const selectChain = useSelectChain(); @@ -105,7 +110,7 @@ export const NetworkCard = observer( - {CHAIN_CONFIGS[chainId]?.prettyName || chainId} + {chainConfigs[chainId]?.prettyName || chainId} {chainId} diff --git a/src/lib/layout/network-menu/network-card/NetworkCardCta.tsx b/src/lib/layout/network-menu/network-card/NetworkCardCta.tsx index a18a5a920..15c9a51cd 100644 --- a/src/lib/layout/network-menu/network-card/NetworkCardCta.tsx +++ b/src/lib/layout/network-menu/network-card/NetworkCardCta.tsx @@ -2,10 +2,13 @@ import { Flex, useToast } from "@chakra-ui/react"; import { observer } from "mobx-react-lite"; import { useCallback } from "react"; -import { CHAIN_CONFIGS } from "config/chain"; -import { useMobile } from "lib/app-provider"; +import { + useChainConfigs, + useInternalNavigate, + useMobile, +} from "lib/app-provider"; import { CustomIcon } from "lib/components/icon"; -import { useNetworkStore } from "lib/providers/store"; +import { useLocalChainConfigStore, useNetworkStore } from "lib/providers/store"; interface NetworkCardCtaProps { chainId: string; @@ -15,7 +18,11 @@ interface NetworkCardCtaProps { export const NetworkCardCta = observer( ({ chainId, isSelected, isDraggable }: NetworkCardCtaProps) => { + const navigate = useInternalNavigate(); + const { chainConfigs } = useChainConfigs(); + const { isLocalChainIdExist } = useLocalChainConfigStore(); const isMobile = useMobile(); + const isEditable = isLocalChainIdExist(chainId); const { isNetworkPinned, pinNetwork, removeNetwork } = useNetworkStore(); const toast = useToast({ status: "success", @@ -30,10 +37,10 @@ export const NetworkCardCta = observer( e.stopPropagation(); pinNetwork(chainId); toast({ - title: `Pinned \u2018${CHAIN_CONFIGS[chainId]?.prettyName}\u2019 successfully`, + title: `Pinned \u2018${chainConfigs[chainId]?.prettyName}\u2019 successfully`, }); }, - [pinNetwork, chainId, toast] + [pinNetwork, chainId, toast, chainConfigs] ); const handleRemove = useCallback( @@ -41,10 +48,10 @@ export const NetworkCardCta = observer( e.stopPropagation(); removeNetwork(chainId); toast({ - title: `\u2018${CHAIN_CONFIGS[chainId]?.prettyName}\u2019 is removed from Pinned Networks`, + title: `\u2018${chainConfigs[chainId]?.prettyName}\u2019 is removed from Pinned Networks`, }); }, - [removeNetwork, chainId, toast] + [removeNetwork, chainId, toast, chainConfigs] ); const opacityStyle = { @@ -85,6 +92,21 @@ export const NetworkCardCta = observer( )} + {isEditable && ( + + navigate({ + pathname: "/custom-network/edit/[chainId]", + query: { + chainId, + }, + }) + } + > + + + )} ); } diff --git a/src/lib/pages/admin/index.tsx b/src/lib/pages/admin/index.tsx index 9c2e534a0..415a3a586 100644 --- a/src/lib/pages/admin/index.tsx +++ b/src/lib/pages/admin/index.tsx @@ -14,6 +14,7 @@ import { useValidateAddress, useWasmConfig, } from "lib/app-provider"; +import ActionPageContainer from "lib/components/ActionPageContainer"; import { ConnectWalletAlert } from "lib/components/ConnectWalletAlert"; import { ContractInputSection } from "lib/components/ContractInputSection"; import { ContractSelectSection } from "lib/components/ContractSelectSection"; @@ -24,7 +25,6 @@ import { TextInput } from "lib/components/forms"; import { CelatoneSeo } from "lib/components/Seo"; import { TierSwitcher } from "lib/components/TierSwitcher"; import { UserDocsLink } from "lib/components/UserDocsLink"; -import WasmPageContainer from "lib/components/WasmPageContainer"; import { useTxBroadcast } from "lib/hooks"; import { useContractData } from "lib/services/wasm/contract"; import type { BechAddr, BechAddr32 } from "lib/types"; @@ -171,7 +171,7 @@ const UpdateAdmin = () => { }, [contractAddressParam, router.isReady]); return ( - + @@ -238,7 +238,7 @@ const UpdateAdmin = () => { > Update Admin - + ); }; diff --git a/src/lib/pages/custom-network/edit/components/ExportNetworkConfig.tsx b/src/lib/pages/custom-network/edit/components/ExportNetworkConfig.tsx new file mode 100644 index 000000000..19a604720 --- /dev/null +++ b/src/lib/pages/custom-network/edit/components/ExportNetworkConfig.tsx @@ -0,0 +1,5 @@ +import { Flex } from "@chakra-ui/react"; + +export const ExportNetworkConfig = () => { + return Export Json; +}; diff --git a/src/lib/pages/custom-network/edit/components/UpdateGasFeeDetails.tsx b/src/lib/pages/custom-network/edit/components/UpdateGasFeeDetails.tsx new file mode 100644 index 000000000..a88f17a4c --- /dev/null +++ b/src/lib/pages/custom-network/edit/components/UpdateGasFeeDetails.tsx @@ -0,0 +1,39 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Flex, + Text, +} from "@chakra-ui/react"; + +import { CustomNetworkSubheader } from "lib/components/custom-network"; + +export const UpdateGasFeeDetails = () => { + return ( + + + + form goes here + + + + form goes here + + + + + Advanced Options + + + + + inputs + + + + + + ); +}; diff --git a/src/lib/pages/custom-network/edit/components/UpdateNetworkDetails.tsx b/src/lib/pages/custom-network/edit/components/UpdateNetworkDetails.tsx new file mode 100644 index 000000000..04302cb46 --- /dev/null +++ b/src/lib/pages/custom-network/edit/components/UpdateNetworkDetails.tsx @@ -0,0 +1,15 @@ +import { Flex } from "@chakra-ui/react"; + +import { CustomNetworkSubheader } from "lib/components/custom-network"; + +export const UpdateNetworkDetails = () => { + return ( + + + form goes here + + ); +}; diff --git a/src/lib/pages/custom-network/edit/components/UpdateSupportedFeatures.tsx b/src/lib/pages/custom-network/edit/components/UpdateSupportedFeatures.tsx new file mode 100644 index 000000000..62033a2b1 --- /dev/null +++ b/src/lib/pages/custom-network/edit/components/UpdateSupportedFeatures.tsx @@ -0,0 +1,15 @@ +import { Flex } from "@chakra-ui/react"; + +import { CustomNetworkSubheader } from "lib/components/custom-network"; + +export const UpdateSupportedFeatures = () => { + return ( + + + form goes here + + ); +}; diff --git a/src/lib/pages/custom-network/edit/components/UpdateWalletRegistry.tsx b/src/lib/pages/custom-network/edit/components/UpdateWalletRegistry.tsx new file mode 100644 index 000000000..c35be80e2 --- /dev/null +++ b/src/lib/pages/custom-network/edit/components/UpdateWalletRegistry.tsx @@ -0,0 +1,21 @@ +import { Flex } from "@chakra-ui/react"; + +import { CustomNetworkSubheader } from "lib/components/custom-network"; + +export const UpdateWalletRegistry = () => { + return ( + + + + form goes here + + + + form goes here + + + ); +}; diff --git a/src/lib/pages/custom-network/edit/index.tsx b/src/lib/pages/custom-network/edit/index.tsx new file mode 100644 index 000000000..83b2947c5 --- /dev/null +++ b/src/lib/pages/custom-network/edit/index.tsx @@ -0,0 +1,187 @@ +import { Button, Grid, Heading, Stack } from "@chakra-ui/react"; +import { isUndefined } from "lodash"; +import { useRouter } from "next/router"; +import { z } from "zod"; + +import { useAllowCustomNetworks } from "lib/app-provider"; +import ActionPageContainer from "lib/components/ActionPageContainer"; +import { CustomNetworkPageHeader } from "lib/components/custom-network"; +// import { CustomTab } from "lib/components/CustomTab"; +import { CustomIcon } from "lib/components/icon"; +import JsonReadOnly from "lib/components/json/JsonReadOnly"; +import { RemoveChainConfigModal } from "lib/components/modal/RemoveChainConfigModal"; +import { InvalidState } from "lib/components/state"; +import { useLocalChainConfigStore } from "lib/providers/store"; +import { jsonPrettify } from "lib/utils"; + +// import { ExportNetworkConfig } from "./components/ExportNetworkConfig"; +// import { UpdateGasFeeDetails } from "./components/UpdateGasFeeDetails"; +// import { UpdateNetworkDetails } from "./components/UpdateNetworkDetails"; +// import { UpdateSupportedFeatures } from "./components/UpdateSupportedFeatures"; +// import { UpdateWalletRegistry } from "./components/UpdateWalletRegistry"; + +// const StyledCustomTab = chakra(CustomTab, { +// baseStyle: { +// border: "unset", +// borderRadius: "4px", +// _selected: { bgColor: "gray.800" }, +// _hover: { bgColor: "gray.700" }, +// }, +// }); + +// const StyledTabPanel = chakra(TabPanel, { +// baseStyle: { +// p: 0, +// width: "550px", +// minWidth: "550px", +// }, +// }); + +// const TabMenu = [ +// { name: "Network Details", key: "network-details" }, +// { name: "Supported Features", key: "supported-features" }, +// { name: "Gas & Fee Details", key: "gas-fee-details" }, +// { name: "Wallet Registry", key: "wallet-registry" }, +// ]; + +const InvalidChainId = () => ; + +interface NetworkConfigBodyProps { + chainId: string; +} + +const NetworkConfigBody = ({ chainId }: NetworkConfigBodyProps) => { + const { getLocalChainConfig } = useLocalChainConfigStore(); + const chainConfig = getLocalChainConfig(chainId); + + // const leftButtonProps = { + // label: "Cancel", + // action: () => {}, + // variant: "outline-secondary", + // }; + + // const rightButtonProps = { + // label: "Update", + // action: () => {}, + // variant: "primary", + // }; + + if (isUndefined(chainConfig)) return ; + + return ( + + + {/* TODO: switch back to templateColumns="2fr 5fr" when left panel is available */} + + {/* } + > + Remove Network + + } + /> */} + + + Current Configuration in JSON + + + } + > + Remove Network + + } + /> + + + + ); + + // return ( + // <> + // + // + // + // + // + // {TabMenu.map((item) => ( + // {item.name} + // ))} + // + // + // Export JSON Soon + // + // + // + // + // + // + // + // + // + // + // + // + // " + // + // + // + // + // + // + // + // + // + // + // + // + // + // ); +}; + +export const NetworkConfig = () => { + useAllowCustomNetworks({ shouldRedirect: true }); + const router = useRouter(); + const validated = z.string().safeParse(router.query.chainId); + + if (!validated.success) return ; + + return ; +}; diff --git a/src/lib/pages/custom-network/index.tsx b/src/lib/pages/custom-network/index.tsx new file mode 100644 index 000000000..5a10e8606 --- /dev/null +++ b/src/lib/pages/custom-network/index.tsx @@ -0,0 +1,48 @@ +import { Alert, AlertDescription, Flex, Heading, Text } from "@chakra-ui/react"; + +import { useAllowCustomNetworks, useInternalNavigate } from "lib/app-provider"; +import ActionPageContainer from "lib/components/ActionPageContainer"; +import { ButtonCard } from "lib/components/ButtonCard"; +import { CelatoneSeo } from "lib/components/Seo"; + +export const AddNetwork = () => { + useAllowCustomNetworks({ shouldRedirect: true }); + const navigate = useInternalNavigate(); + + return ( + + + + + + Add Custom Minitia + + + + + Please note that the custom Minitia you add on our website will + only be stored locally on your device. + + + + + + navigate({ pathname: "/custom-network/add/manual" })} + /> + {/* // TODO: Remove tag and disabled to enable import JSON option entry point */} + navigate({ pathname: "/custom-network/add/json" })} + tagLabel="Coming Soon!" + hasIcon={false} + /> + + + + ); +}; diff --git a/src/lib/pages/custom-network/manual/components/AddNetworkForm.tsx b/src/lib/pages/custom-network/manual/components/AddNetworkForm.tsx new file mode 100644 index 000000000..1e68ee014 --- /dev/null +++ b/src/lib/pages/custom-network/manual/components/AddNetworkForm.tsx @@ -0,0 +1,43 @@ +import type { + Control, + FieldErrors, + UseFormSetValue, + UseFormTrigger, +} from "react-hook-form"; + +import type { AddNetworkManualForm } from "../types"; + +import GasFeeDetails from "./GasFeeDetails"; +import { NetworkDetails } from "./NetworkDetails"; +import { WalletRegistry } from "./WalletRegistry"; + +interface AddNetworkFormProps { + currentStepIndex: number; + control: Control; + errors: FieldErrors; + setValue: UseFormSetValue; + trigger: UseFormTrigger; +} + +export const AddNetworkForm = ({ + currentStepIndex, + control, + errors, + setValue, + trigger, +}: AddNetworkFormProps) => { + if (currentStepIndex === 0) + return ; + + if (currentStepIndex === 1) + return ( + + ); + + return ; +}; diff --git a/src/lib/pages/custom-network/manual/components/AddNetworkStepper.tsx b/src/lib/pages/custom-network/manual/components/AddNetworkStepper.tsx new file mode 100644 index 000000000..592981a9a --- /dev/null +++ b/src/lib/pages/custom-network/manual/components/AddNetworkStepper.tsx @@ -0,0 +1,43 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { getStepStyles } from "../hooks/utils"; + +const steps = [ + { label: "Network Details" }, + { label: "Gas & Fee Details" }, + { label: "Wallet Registry" }, +]; + +interface AddNetworkStepperProps { + currentStepIndex: number; +} + +export const AddNetworkStepper = ({ + currentStepIndex, +}: AddNetworkStepperProps) => ( + + {steps.map((step, index) => { + const { bgColor, textColor, borderColor, content } = getStepStyles( + index, + currentStepIndex + ); + + return ( + + {content} + {step.label} + + ); + })} + +); diff --git a/src/lib/pages/custom-network/manual/components/GasFeeDetails.tsx b/src/lib/pages/custom-network/manual/components/GasFeeDetails.tsx new file mode 100644 index 000000000..558ad3a0a --- /dev/null +++ b/src/lib/pages/custom-network/manual/components/GasFeeDetails.tsx @@ -0,0 +1,329 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Flex, + Grid, + Radio, + RadioGroup, + Text, +} from "@chakra-ui/react"; +import { useEffect } from "react"; +import { useWatch } from "react-hook-form"; +import type { + Control, + FieldErrors, + UseFormSetValue, + UseFormTrigger, +} from "react-hook-form"; + +import type { AddNetworkManualForm } from "../types"; +import { + CustomNetworkPageHeader, + CustomNetworkSubheader, +} from "lib/components/custom-network"; +import { ControllerInput } from "lib/components/forms"; + +export enum GasPriceConfiguration { + STANDARD = "standard", + CUSTOM = "custom", +} + +interface GasFeeDetailsProps { + control: Control; + errors: FieldErrors; + setValue: UseFormSetValue; + trigger: UseFormTrigger; +} + +const restrictedNumberInputParams = { + maxIntegerPoints: 20, + maxDecimalPoints: 20, +}; + +const GasOptionStandard = ({ + control, + errors, +}: Omit) => ( + +); + +const GasOptionCustom = ({ + control, + errors, +}: Omit) => ( + <> + + + + Fixed Minimum Gas Price + + + (Required) + + + + + + + + Low Gas Price + + + (Required) + + + + + + + + Average Gas Price + + + (Required) + + + + + + + + High Gas Price + + + (Required) + + + + + +); + +const GasFeeDetails = ({ + control, + errors, + setValue, + trigger, +}: GasFeeDetailsProps) => { + const { + gasPrice, + gasConfig, + fixedMinimumGasPrice, + lowGasPrice, + averageGasPrice, + highGasPrice, + } = useWatch({ control }); + + useEffect(() => { + if (!gasPrice) return; + + if (gasConfig === GasPriceConfiguration.CUSTOM) { + const isCustomValueEqual = + fixedMinimumGasPrice === lowGasPrice && + lowGasPrice === averageGasPrice && + averageGasPrice === highGasPrice; + + setValue("gasPrice", isCustomValueEqual ? fixedMinimumGasPrice : ""); + trigger(); + + return; + } + + if (gasConfig === GasPriceConfiguration.STANDARD) { + setValue("fixedMinimumGasPrice", gasPrice); + setValue("lowGasPrice", gasPrice); + setValue("averageGasPrice", gasPrice); + setValue("highGasPrice", gasPrice); + trigger(); + } + }, [ + gasConfig, + setValue, + gasPrice, + trigger, + fixedMinimumGasPrice, + lowGasPrice, + averageGasPrice, + highGasPrice, + ]); + + return ( + + + + + + + + + + + + + + + setValue("gasConfig", nextVal as GasPriceConfiguration) + } + value={gasConfig} + > + + + Standard Gas Price + + Set the standard gas price as the default for all gas price + configurations + + + + Custom Gas Prices + + Set the custom value for minimum, low, average, and high gas + price + + + + + {gasConfig === GasPriceConfiguration.STANDARD && ( + + )} + {gasConfig === GasPriceConfiguration.CUSTOM && ( + + )} + + + + + Advanced Options + + + + + + + + + + + + + ); +}; + +export default GasFeeDetails; diff --git a/src/lib/pages/custom-network/manual/components/NetworkDetails.tsx b/src/lib/pages/custom-network/manual/components/NetworkDetails.tsx new file mode 100644 index 000000000..7b682af5d --- /dev/null +++ b/src/lib/pages/custom-network/manual/components/NetworkDetails.tsx @@ -0,0 +1,151 @@ +import { Flex, Grid, Radio, RadioGroup, Text } from "@chakra-ui/react"; +import { useController, useWatch } from "react-hook-form"; +import type { Control, FieldErrors } from "react-hook-form"; + +import { VmType } from "../types"; +import type { AddNetworkManualForm } from "../types"; +import { + CustomNetworkPageHeader, + CustomNetworkSubheader, +} from "lib/components/custom-network"; +import { ControllerInput } from "lib/components/forms"; + +interface NetworkDetailsProps { + control: Control; + errors: FieldErrors; +} + +export const NetworkDetails = ({ control, errors }: NetworkDetailsProps) => { + const vmType = useWatch({ + control, + name: "vmType", + }); + + const { field: vmTypeField } = useController({ + control, + name: "vmType", + }); + + return ( + + + + + vmTypeField.onChange(nextVal)} + value={vmType} + > + + + Move + + + Wasm + + + + + + + + + + + + + + + + + + You can edit these details later. + + + + ); +}; diff --git a/src/lib/pages/custom-network/manual/components/SuccessAddCustomMinitiaModal.tsx b/src/lib/pages/custom-network/manual/components/SuccessAddCustomMinitiaModal.tsx new file mode 100644 index 000000000..9f1aee34b --- /dev/null +++ b/src/lib/pages/custom-network/manual/components/SuccessAddCustomMinitiaModal.tsx @@ -0,0 +1,104 @@ +import { + Button, + Heading, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Spinner, + Stack, + Text, +} from "@chakra-ui/react"; +import { useEffect, useState } from "react"; + +import { CustomIcon } from "lib/components/icon"; + +interface SuccessAddCustomMinitiaModalProps { + isOpen: boolean; + onClose: () => void; + prettyName: string; + chainId: string; +} + +export const SuccessAddCustomMinitiaModal = ({ + isOpen, + onClose, + prettyName, + chainId, +}: SuccessAddCustomMinitiaModalProps) => { + const [fakeLoading, setFakeLoading] = useState(false); + + useEffect(() => { + setFakeLoading(true); + + const timeoutId = setTimeout(() => { + setFakeLoading((newFakeLoading) => !newFakeLoading); + }, 2000); + + return () => clearTimeout(timeoutId); + }, [isOpen]); + + return ( + + + + {fakeLoading ? ( + <> + + + + Adding Custom Minitia... + + + + + Your minitia’s information is being processed, and the scan’s + for minitia will be ready shortly. Please do not close the + browser during this process. + + + + ) : ( + <> + + + + “{prettyName}” is added! + + + + + Your custom minitia is added to the InitiaScan locally on your + device. You also can download the configuration in to JSON file + to import them in other devices. + + + + + + + )} + + + ); +}; diff --git a/src/lib/pages/custom-network/manual/components/WalletRegistry.tsx b/src/lib/pages/custom-network/manual/components/WalletRegistry.tsx new file mode 100644 index 000000000..73d7d51ef --- /dev/null +++ b/src/lib/pages/custom-network/manual/components/WalletRegistry.tsx @@ -0,0 +1,330 @@ +import { + Button, + Divider, + Flex, + Grid, + Heading, + IconButton, + SkeletonText, + Stack, + Text, +} from "@chakra-ui/react"; +import { useEffect } from "react"; +import { useController, useFieldArray, useWatch } from "react-hook-form"; +import type { Control, FieldErrors } from "react-hook-form"; + +import type { AddNetworkManualForm } from "../types"; +import { + CustomNetworkPageHeader, + CustomNetworkSubheader, +} from "lib/components/custom-network"; +import { ControllerInput } from "lib/components/forms"; +import { CustomIcon } from "lib/components/icon"; +import { LabelText } from "lib/components/LabelText"; +import { useAccountBech32 } from "lib/services/account"; + +interface WalletRegistryProps { + control: Control; + errors: FieldErrors; +} + +interface DenomUnitsProps extends WalletRegistryProps { + assetIndex: number; +} + +const DenomUnits = ({ control, assetIndex, errors }: DenomUnitsProps) => { + const { fields, append, remove } = useFieldArray({ + control, + name: `assets.${assetIndex}.denoms`, + }); + + return fields.length ? ( + + {fields.map((denom, index) => ( + + + + Denom Unit + + } + onClick={() => remove(index)} + variant="ghost-gray" + size="sm" + /> + + + + + + + ))} + + + ) : ( + + + + ); +}; + +export const WalletRegistry = ({ control, errors }: WalletRegistryProps) => { + const [lcdUrl, bech32Prefix] = useWatch({ + control, + name: ["lcdUrl", "bech32Prefix"], + }); + + const { data: accountBech32, isLoading: isAccountBech32Loading } = + useAccountBech32(lcdUrl); + + const { + field: { onChange }, + } = useController({ + name: "bech32Prefix", + control, + }); + + useEffect(() => { + if (accountBech32?.bech32Prefix === bech32Prefix) return; + onChange(accountBech32?.bech32Prefix ?? "init"); + }, [accountBech32?.bech32Prefix, bech32Prefix, onChange]); + + const { fields, append, remove } = useFieldArray({ + control, + name: "assets", + }); + + return ( + + + + + + + + {isAccountBech32Loading ? ( + + ) : ( + accountBech32?.bech32Prefix ?? "init" + )} + + 118 + + + + Account address in this Minitia will look like this: + + {isAccountBech32Loading ? ( + + ) : ( + + {accountBech32?.bech32Prefix ?? "init"} + 1cvhde2nst3qewz8x58m6tuupfk08zspeev4ud3 + + )} + + {!isAccountBech32Loading && !accountBech32 && ( + <> + + + * Bech32 and Slip44 data is not available from LCD. The input + above will be set as default. + + + )} + + + + + {fields.length ? ( + + {fields.map((asset, index) => ( + + + + Asset + + } + onClick={() => remove(index)} + variant="ghost-gray" + size="sm" + /> + + + + + + + + + Denom Units + + + + + ))} + + + ) : ( + + + + + + Without asset information, the website remains functional. + However, with the wallet provider, assets may appear in a long + format. + + + )} + + + ); +}; diff --git a/src/lib/pages/custom-network/manual/components/index.ts b/src/lib/pages/custom-network/manual/components/index.ts new file mode 100644 index 000000000..c4934e039 --- /dev/null +++ b/src/lib/pages/custom-network/manual/components/index.ts @@ -0,0 +1,6 @@ +export * from "./AddNetworkStepper"; +export * from "./GasFeeDetails"; +export * from "./NetworkDetails"; +export * from "./WalletRegistry"; +export * from "./SuccessAddCustomMinitiaModal"; +export * from "./AddNetworkForm"; diff --git a/src/lib/pages/custom-network/manual/hooks/useNetworkStepper.ts b/src/lib/pages/custom-network/manual/hooks/useNetworkStepper.ts new file mode 100644 index 000000000..995f355e9 --- /dev/null +++ b/src/lib/pages/custom-network/manual/hooks/useNetworkStepper.ts @@ -0,0 +1,58 @@ +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +import { useCelatoneApp, useInternalNavigate } from "lib/app-provider"; + +export const useNetworkStepper = (limit: number, handleSubmit: () => void) => { + const router = useRouter(); + const navigate = useInternalNavigate(); + const currentStep = router.query.step; + const { currentChainId } = useCelatoneApp(); + + useEffect(() => { + if (Number(currentStep) === 1) return; + + router.push( + { + query: { + network: currentChainId, + step: 1, + }, + }, + undefined, + { shallow: true } + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleNext = () => + currentStep && Number(currentStep) < limit + ? router.push( + { + query: { + network: currentChainId, + step: Number(currentStep) + 1, + }, + }, + undefined, + { shallow: true } + ) + : handleSubmit(); + + const handlePrevious = () => { + if (Number(currentStep) === 1) { + navigate({ pathname: "/custom-network/add" }); + return; + } + + router.back(); + }; + + return { + currentStepIndex: Number(currentStep) - 1, + handleNext, + handlePrevious, + hasNext: Number(currentStep) < limit, + hasPrevious: Number(currentStep) > 1, + }; +}; diff --git a/src/lib/pages/custom-network/manual/hooks/utils.tsx b/src/lib/pages/custom-network/manual/hooks/utils.tsx new file mode 100644 index 000000000..f95721fe7 --- /dev/null +++ b/src/lib/pages/custom-network/manual/hooks/utils.tsx @@ -0,0 +1,53 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { CustomIcon } from "lib/components/icon"; + +export const getStepStyles = (index: number, currentStep: number) => { + const baseStyles = { + borderRadius: "50%", + width: "24px", + height: "24px", + alignItems: "center", + justifyContent: "center", + }; + + switch (true) { + case index < currentStep: + return { + bgColor: "gray.900", + borderColor: "text.main", + textColor: "text.main", + content: ( + + + + ), + }; + case index === currentStep: + return { + bgColor: "gray.800", + borderColor: "text.main", + textColor: "text.main", + content: ( + + + {index + 1} + + + ), + }; + default: + return { + bgColor: "gray.900", + borderColor: "gray.800", + textColor: "text.dark", + content: ( + + + {index + 1} + + + ), + }; + } +}; diff --git a/src/lib/pages/custom-network/manual/index.tsx b/src/lib/pages/custom-network/manual/index.tsx new file mode 100644 index 000000000..fc830429b --- /dev/null +++ b/src/lib/pages/custom-network/manual/index.tsx @@ -0,0 +1,203 @@ +import { Flex, useDisclosure } from "@chakra-ui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; + +import { useAllowCustomNetworks, useChainConfigs } from "lib/app-provider"; +import ActionPageContainer from "lib/components/ActionPageContainer"; +import { CustomIcon } from "lib/components/icon"; +import { FooterCta } from "lib/components/layouts"; +import { CelatoneSeo } from "lib/components/Seo"; +import { useLocalChainConfigStore } from "lib/providers/store"; + +import { + AddNetworkForm, + AddNetworkStepper, + SuccessAddCustomMinitiaModal, +} from "./components"; +import { useNetworkStepper } from "./hooks/useNetworkStepper"; +import { + VmType, + zAddNetworkManualChainConfigJson, + zAddNetworkManualForm, + zGasFeeDetailsForm, + zNetworkDetailsForm, + zWalletRegistryForm, +} from "./types"; +import type { AddNetworkManualForm } from "./types"; + +export const AddNetworkManual = () => { + useAllowCustomNetworks({ shouldRedirect: true }); + const { isOpen, onClose, onOpen } = useDisclosure(); + const { addLocalChainConfig } = useLocalChainConfigStore(); + const { isChainIdExist, isPrettyNameExist } = useChainConfigs(); + + const { + control, + handleSubmit, + formState: { errors }, + watch, + setValue, + trigger, + } = useForm({ + resolver: zodResolver( + zAddNetworkManualForm({ isChainIdExist, isPrettyNameExist }) + ), + mode: "all", + reValidateMode: "onChange", + defaultValues: { + networkName: "", + lcdUrl: "", + rpcUrl: "", + chainId: "", + registryChainName: "", + logoUri: "", + vmType: VmType.MOVE, + gasAdjustment: 1.5, + maxGasLimit: 25000000, + feeTokenDenom: "umin", + gasConfig: "standard", + gasPrice: 0.15, + fixedMinimumGasPrice: 0.15, + lowGasPrice: 0.15, + averageGasPrice: 0.15, + highGasPrice: 0.15, + gasForCosmosSend: "", + gasForIbc: "", + bech32Prefix: "init", + slip44: 118, + assets: [], + }, + }); + + const { + vmType, + networkName, + lcdUrl, + rpcUrl, + chainId, + registryChainName, + logoUri, + gasAdjustment, + maxGasLimit, + feeTokenDenom, + gasConfig, + gasPrice, + fixedMinimumGasPrice, + lowGasPrice, + averageGasPrice, + highGasPrice, + gasForCosmosSend, + gasForIbc, + bech32Prefix, + slip44, + assets, + } = watch(); + + const handleSubmitForm = (data: AddNetworkManualForm) => { + addLocalChainConfig( + data.chainId, + zAddNetworkManualChainConfigJson({ + isChainIdExist, + isPrettyNameExist, + }).parse(data) + ); + + onOpen(); + }; + + const { currentStepIndex, handleNext, handlePrevious, hasNext, hasPrevious } = + useNetworkStepper(3, handleSubmit(handleSubmitForm)); + + const isFormDisabled = () => { + if (currentStepIndex === 0) + return !zNetworkDetailsForm({ + isChainIdExist, + isPrettyNameExist, + }).safeParse({ + vmType, + networkName, + lcdUrl, + rpcUrl, + chainId, + registryChainName, + logoUri, + }).success; + + if (currentStepIndex === 1) + return !zGasFeeDetailsForm.safeParse({ + gasAdjustment, + maxGasLimit, + feeTokenDenom, + gasConfig, + gasPrice, + fixedMinimumGasPrice, + lowGasPrice, + averageGasPrice, + highGasPrice, + gasForCosmosSend, + gasForIbc, + }).success; + + if (currentStepIndex === 2) + return !zWalletRegistryForm.safeParse({ + bech32Prefix, + slip44, + assets, + }).success; + + return false; + }; + + const handleActionLabel = () => { + if (currentStepIndex === 2) return "Save new Minitia"; + + return "Next"; + }; + + return ( + <> + + + + + + + + + ) : undefined, + }} + cancelLabel={hasPrevious ? "Previous" : "Cancel"} + actionButton={{ + onClick: handleNext, + isDisabled: isFormDisabled(), + rightIcon: hasNext ? ( + + ) : undefined, + }} + actionLabel={handleActionLabel()} + helperText="The added custom Minitia on Initiascan will be stored locally on your device." + sx={{ + backgroundColor: "background.main", + borderColor: "gray.700", + }} + /> + + + ); +}; diff --git a/src/lib/pages/custom-network/manual/types.ts b/src/lib/pages/custom-network/manual/types.ts new file mode 100644 index 000000000..0afdd2cc3 --- /dev/null +++ b/src/lib/pages/custom-network/manual/types.ts @@ -0,0 +1,290 @@ +import type { ChainConfig } from "@alleslabs/shared"; +import type { RefinementCtx } from "zod"; +import { z, ZodIssueCode } from "zod"; + +const mustBeAlphabetNumberAndSpecialCharacters = + "Must be alphabet (a-z), numbers (0-9), or these special characters: “/”, “:”, “.”, “_”, “-”"; +const mustBeNumbersOnly = "Must be numbers only"; + +const zHttpsUrl = z.string().regex(/^(http|https):\/\/[^\s$.?#].[^\s]*$/, { + message: "Please enter a valid URL", +}); + +const zNumberForm = z + .union([ + z.literal(""), + z.coerce + .number({ + invalid_type_error: mustBeNumbersOnly, + }) + .nonnegative({ message: "Must be greater than 0" }), + ]) + .optional(); + +const zNumberFormRequired = zNumberForm.superRefine((val, ctx) => { + if (val === "") + return ctx.addIssue({ code: ZodIssueCode.custom, message: " " }); + + return true; +}); + +interface ValidateExistingChain { + isChainIdExist: (chainId: string) => boolean; + isPrettyNameExist: (name: string) => boolean; +} + +export enum VmType { + MOVE = "move", + WASM = "wasm", +} + +export const zNetworkDetailsForm = ({ + isChainIdExist, + isPrettyNameExist, +}: ValidateExistingChain) => + z.object({ + vmType: z.nativeEnum(VmType), + networkName: z.string().superRefine((val, ctx) => { + if (val.length > 50) + ctx.addIssue({ + code: ZodIssueCode.custom, + message: `Minitia Name is too long. (${val.length}/50)`, + }); + + if (isPrettyNameExist(val)) + ctx.addIssue({ + code: ZodIssueCode.custom, + message: "This name is already used. Please specify other name.", + }); + + return true; + }), + lcdUrl: zHttpsUrl, + rpcUrl: zHttpsUrl, + chainId: z + .string() + .min(1, { message: " " }) + .superRefine((val, ctx) => { + if (isChainIdExist(val)) + ctx.addIssue({ + code: ZodIssueCode.custom, + message: "This Chain ID is already added.", + }); + + return true; + }), + registryChainName: z.string().regex(/^[a-z0-9]+$/, { + message: "Lower case letter (a-z) or number (0-9)", + }), + logoUri: z.union([zHttpsUrl, z.literal("")]), + }); +export type NetworkDetailsForm = z.infer< + ReturnType +>; + +const zGasFeeDetails = z.object({ + gasAdjustment: zNumberFormRequired, + maxGasLimit: zNumberFormRequired, + feeTokenDenom: z.string().regex(/^[a-z0-9/:._-]+$/, { + message: mustBeAlphabetNumberAndSpecialCharacters, + }), + gasConfig: z.enum(["standard", "custom"]), + gasPrice: zNumberForm, + fixedMinimumGasPrice: zNumberForm, + lowGasPrice: zNumberForm, + averageGasPrice: zNumberForm, + highGasPrice: zNumberForm, + gasForCosmosSend: zNumberForm, + gasForIbc: zNumberForm, +}); + +export type GasFeeDetails = z.infer; + +const gasConfigCustomFormValidator = ( + val: GasFeeDetails, + ctx: RefinementCtx +) => { + if (val.gasConfig === "custom") { + if (val.fixedMinimumGasPrice === "") + ctx.addIssue({ + code: ZodIssueCode.custom, + message: " ", + path: ["fixedMinimumGasPrice"], + }); + + if (val.lowGasPrice === "") + ctx.addIssue({ + code: ZodIssueCode.custom, + message: " ", + path: ["lowGasPrice"], + }); + + if (val.averageGasPrice === "") + ctx.addIssue({ + code: ZodIssueCode.custom, + message: " ", + path: ["averageGasPrice"], + }); + + if (val.highGasPrice === "") + ctx.addIssue({ + code: ZodIssueCode.custom, + message: " ", + path: ["highGasPrice"], + }); + } + + if (val.gasConfig === "standard" && val.gasPrice === "") + ctx.addIssue({ + code: ZodIssueCode.custom, + message: " ", + path: ["gasPrice"], + }); +}; + +export const zGasFeeDetailsForm = zGasFeeDetails.superRefine( + gasConfigCustomFormValidator +); + +const zWalletRegistryAssetDenomForm = z.object({ + denom: z.string().regex(/^[a-z0-9/:._-]+$/, { + message: mustBeAlphabetNumberAndSpecialCharacters, + }), + exponent: zNumberFormRequired, +}); + +const zWalletRegistryAssetForm = z.object({ + name: z.string().regex(/^[a-z-]+$/, { + message: "Please enter only alphabet (a-z) and dash (-) with no spaces", + }), + base: z.string().regex(/^[a-z0-9/:._-]+$/, { + message: mustBeAlphabetNumberAndSpecialCharacters, + }), + symbol: z.string().min(1, { message: " " }), + denoms: z.array(zWalletRegistryAssetDenomForm), +}); + +export const zWalletRegistryForm = z.object({ + bech32Prefix: z.string().regex(/^[a-z]+$/, { + message: "Alphabet (a-z) only", + }), + slip44: z.coerce + .number({ + invalid_type_error: mustBeNumbersOnly, + }) + .int({ + message: "Must be an integer", + }) + .nonnegative({ + message: "Must be greater than 0", + }), + assets: z.array(zWalletRegistryAssetForm), +}); +export type WalletRegistryForm = z.infer; + +export const zAddNetworkManualForm = ({ + isChainIdExist, + isPrettyNameExist, +}: ValidateExistingChain) => + zNetworkDetailsForm({ isChainIdExist, isPrettyNameExist }) + .merge(zGasFeeDetailsForm.innerType()) + .merge(zWalletRegistryForm) + .superRefine(gasConfigCustomFormValidator); + +export type AddNetworkManualForm = z.infer< + ReturnType +>; + +export const zAddNetworkManualChainConfigJson = ({ + isChainIdExist, + isPrettyNameExist, +}: ValidateExistingChain) => + zAddNetworkManualForm({ + isChainIdExist, + isPrettyNameExist, + }).transform((val: AddNetworkManualForm) => ({ + tier: "sequencer", + chainId: val.chainId, + chain: "initia", + registryChainName: val.registryChainName, + prettyName: val.networkName, + logo_URIs: { + png: val.logoUri || undefined, + }, + lcd: val.lcdUrl, + rpc: val.rpcUrl, + graphql: "", + wallets: ["initia", "keplr"], + features: { + faucet: { + enabled: false, + }, + wasm: + val.vmType === VmType.WASM + ? { + enabled: true, + storeCodeMaxFileSize: 1024 * 1024 * 2, + clearAdminGas: 1000000, + } + : { enabled: false }, + move: + val.vmType === VmType.MOVE + ? { + enabled: true, + moduleMaxFileSize: 1_048_576, + } + : { enabled: false }, + pool: { + enabled: false, + }, + publicProject: { + enabled: true, + }, + gov: { + enabled: false, + }, + nft: { + enabled: val.vmType === VmType.MOVE, + }, + }, + gas: { + gasAdjustment: Number(val.gasAdjustment), + maxGasLimit: Number(val.maxGasLimit), + }, + extra: { + isValidatorExternalLink: null, + layer: "2", + }, + network_type: "local", + fees: { + fee_tokens: [ + { + denom: val.feeTokenDenom, + fixed_min_gas_price: Number(val.fixedMinimumGasPrice), + low_gas_price: Number(val.lowGasPrice), + average_gas_price: Number(val.averageGasPrice), + gas_costs: { + cosmos_send: Number(val.gasForCosmosSend), + ibc_transfer: Number(val.gasForIbc), + }, + }, + ], + }, + registry: { + bech32_prefix: val.bech32Prefix, + slip44: val.slip44, + staking: { + staking_tokens: [], + }, + assets: val.assets.map((asset) => ({ + name: asset.name, + base: asset.base, + symbol: asset.symbol, + denom_units: asset.denoms.map((denom) => ({ + denom: denom.denom, + exponent: Number(denom.exponent), + })), + display: asset.symbol, + })), + }, + })); diff --git a/src/lib/pages/deploy-script/index.tsx b/src/lib/pages/deploy-script/index.tsx index 41db5f7b6..7537658cd 100644 --- a/src/lib/pages/deploy-script/index.tsx +++ b/src/lib/pages/deploy-script/index.tsx @@ -10,12 +10,12 @@ import { useSimulateFeeQuery, } from "lib/app-provider"; import { useDeployScriptTx } from "lib/app-provider/tx/script"; +import ActionPageContainer from "lib/components/ActionPageContainer"; import { ConnectWalletAlert } from "lib/components/ConnectWalletAlert"; import { ErrorMessageRender } from "lib/components/ErrorMessageRender"; import { EstimatedFeeRender } from "lib/components/EstimatedFeeRender"; import { CelatoneSeo } from "lib/components/Seo"; import { UserDocsLink } from "lib/components/UserDocsLink"; -import WasmPageContainer from "lib/components/WasmPageContainer"; import { useTxBroadcast } from "lib/hooks"; import type { AbiFormData, ExposedFunction, Option } from "lib/types"; import { composeScriptMsg, getAbiInitialData } from "lib/utils"; @@ -137,7 +137,7 @@ export const DeployScript = () => { return ( <> - + Script @@ -214,7 +214,7 @@ export const DeployScript = () => { alignSelf="flex-start" /> )} - +