diff --git a/.github/workflows/main.yml b/.github/workflows/changelog.yml similarity index 95% rename from .github/workflows/main.yml rename to .github/workflows/changelog.yml index e5a6ced5d..eb0338232 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/changelog.yml @@ -1,4 +1,4 @@ -name: CheckChangelog +name: Check Changelog on: pull_request: types: [assigned, opened, synchronize, reopened, labeled, unlabeled] diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml new file mode 100644 index 000000000..94185a819 --- /dev/null +++ b/.github/workflows/jest.yml @@ -0,0 +1,17 @@ +name: Unit Tests +on: + pull_request: + types: [assigned, opened, synchronize, reopened, labeled, unlabeled] + branches: + - main + - develop + +jobs: + Jest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Run Tests + run: | + yarn + yarn test diff --git a/.gitignore b/.gitignore index 946a6e448..b0b829f40 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ next.config.original.js .sentryclirc next.config.original.js next.config.wizardcopy.js + +# yarn +/.yarn +.yarnrc.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 055cdaf01..3d71df88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,102 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Features + +### Improvements + +### Bug fixes + +## v1.1.0 + +### Features + +- [#397](https://github.com/alleslabs/celatone-frontend/pull/397) Implement first version of Osmosis v16 pool +- [#358](https://github.com/alleslabs/celatone-frontend/pull/358) Add Amp for Pool Detail page +- [#355](https://github.com/alleslabs/celatone-frontend/pull/355) Add Amp for Pool List page +- [#294](https://github.com/alleslabs/celatone-frontend/pull/294) Add Pool related txs table +- [#296](https://github.com/alleslabs/celatone-frontend/pull/296) Add pool header and pool assets section for pool details page +- [#295](https://github.com/alleslabs/celatone-frontend/pull/295) Add expand/collapse all for unsupported pool list +- [#277](https://github.com/alleslabs/celatone-frontend/pull/277) Wire up data for pool navigation page +- [#276](https://github.com/alleslabs/celatone-frontend/pull/276) Add Pool navigation and pool detail data +- [#270](https://github.com/alleslabs/celatone-frontend/pull/270) Add Pool navigation and detail page +- [#418](https://github.com/alleslabs/celatone-frontend/pull/418) Add gitignore for new yarn version +- [#406](https://github.com/alleslabs/celatone-frontend/pull/406) add test cases for `utils/formatter/token.ts` +- [#398](https://github.com/alleslabs/celatone-frontend/pull/398) Setup Jest and sample tests +- [#411](https://github.com/alleslabs/celatone-frontend/pull/411) Add override api endpoint +- [#385](https://github.com/alleslabs/celatone-frontend/pull/385) Upgrade cosmos kit major version and replace hooks +- [#380](https://github.com/alleslabs/celatone-frontend/pull/380) Support local network +- [#363](https://github.com/alleslabs/celatone-frontend/pull/363) Add config not found page and rewrite network logic +- [#343](https://github.com/alleslabs/celatone-frontend/pull/343) Apply fetching mechanism and keyboard arrow navigation to searchbar +- [#384](https://github.com/alleslabs/celatone-frontend/pull/384) New pagination style +- [#388](https://github.com/alleslabs/celatone-frontend/pull/388) Save Navbar expand/collapse state in the local state +- [#407](https://github.com/alleslabs/celatone-frontend/pull/407) Add code upload error message under the dropzone +- [#372](https://github.com/alleslabs/celatone-frontend/pull/372) Add code hash to code details and upload section +- [#329](https://github.com/alleslabs/celatone-frontend/pull/329) Add allowed user to store code flow +- [#321](https://github.com/alleslabs/celatone-frontend/pull/321) Add amplitude to proposal to store code page +- [#274](https://github.com/alleslabs/celatone-frontend/pull/274) Add proposal to store code page +- [#279](https://github.com/alleslabs/celatone-frontend/pull/279) Add instantiate permission to msg store code, change error display design, and upgrade cosmjs to version 0.30.1 +- [#366](https://github.com/alleslabs/celatone-frontend/pull/366) Add recent contracts + +### Improvements + +- [#428](https://github.com/alleslabs/celatone-frontend/pull/428) Get all validators from graphql +- [#417](https://github.com/alleslabs/celatone-frontend/pull/417) Support responsive and add new theme +- [#283](https://github.com/alleslabs/celatone-frontend/pull/283) Change unsupported token icon render logic +- [#420](https://github.com/alleslabs/celatone-frontend/pull/420) Unify create proposal page layout +- [#416](https://github.com/alleslabs/celatone-frontend/pull/416) Remove the old redundant useSimulateFee hook +- [#413](https://github.com/alleslabs/celatone-frontend/pull/413) Add jest test cases for date utils +- [#404](https://github.com/alleslabs/celatone-frontend/pull/404) Use internal navigate instead of app link for block navigation +- [#396](https://github.com/alleslabs/celatone-frontend/pull/396) Refactor useConfig, disable wasm related tabs on the public project page +- [#392](https://github.com/alleslabs/celatone-frontend/pull/392) Refactor proposal table and fix empty state of the proposal list table +- [#374](https://github.com/alleslabs/celatone-frontend/pull/374) Remove testnet, mainnet concepts and use permission from params +- [#369](https://github.com/alleslabs/celatone-frontend/pull/369) Implement Wasm feature from config +- [#359](https://github.com/alleslabs/celatone-frontend/pull/359) Remove hardcode constant (length) and use from config +- [#367](https://github.com/alleslabs/celatone-frontend/pull/367) Update osmosis testnet 5 config and use explorer url from config instead +- [#368](https://github.com/alleslabs/celatone-frontend/pull/368) Use chain name from config for Meta instead of env variable +- [#336](https://github.com/alleslabs/celatone-frontend/pull/336) Get address type length from example addresses instead of hardcode +- [#354](https://github.com/alleslabs/celatone-frontend/pull/354) Remove useChainId and use currentChainId from config +- [#341](https://github.com/alleslabs/celatone-frontend/pull/341) Apply faucet info from chain config +- [#338](https://github.com/alleslabs/celatone-frontend/pull/338) Use gas from chain config +- [#333](https://github.com/alleslabs/celatone-frontend/pull/333) Update endpoints including LCD, RPC, Graphql +- [#335](https://github.com/alleslabs/celatone-frontend/pull/335) Refactor hardcoded api route to utils +- [#373](https://github.com/alleslabs/celatone-frontend/pull/373) Add view in Json for assets in account details page +- [#376](https://github.com/alleslabs/celatone-frontend/pull/376) Fix pluralize and capitalize +- [#401](https://github.com/alleslabs/celatone-frontend/pull/401) Add permission chip to code selection box +- [#386](https://github.com/alleslabs/celatone-frontend/pull/386) Handle uppercase address +- [#382](https://github.com/alleslabs/celatone-frontend/pull/382) Add pool manager v15 msgs to tx details +- [#371](https://github.com/alleslabs/celatone-frontend/pull/371) Refactor assign me component and fix color in redelegation page +- [#342](https://github.com/alleslabs/celatone-frontend/pull/342) Add fallback n/a token on asset icon on asset box +- [#331](https://github.com/alleslabs/celatone-frontend/pull/331) Add validation check for builder in proposal to store code page +- [#324](https://github.com/alleslabs/celatone-frontend/pull/324) Add deposit/voting period from gov params and add minimum required alert for proposal to store code +- [#357](https://github.com/alleslabs/celatone-frontend/pull/357) Abstract color, typeface, images to theme config +- [#352](https://github.com/alleslabs/celatone-frontend/pull/352) Refactor/Abstract styling (color system, font weight and more) +- [#347](https://github.com/alleslabs/celatone-frontend/pull/347) Move tx table accordion arrow to the front and refactor block txs table + +### Bug fixes + +- [#410](https://github.com/alleslabs/celatone-frontend/pull/410) Remove hardcode precision in attach funds dropdown selection and get assets from API, delete microfy and demicrofy function, remove useChainRecordAsset +- [#434](https://github.com/alleslabs/celatone-frontend/pull/434) Fix stepper item bg color +- [#429](https://github.com/alleslabs/celatone-frontend/pull/429) Fix duration formatter, type and add migrate tab to balancer pool +- [#427](https://github.com/alleslabs/celatone-frontend/pull/427) Fix upload access endpoint, Public project searchbar bug, account tx bug +- [#425](https://github.com/alleslabs/celatone-frontend/pull/425) Fix reset past tx filter state and permission alert on migration page +- [#365](https://github.com/alleslabs/celatone-frontend/pull/365) Fix pool page UI including helper text gap, icon alignment in pool asset table, empty state border, add message type, input selection icon, and accordion alignment +- [#362](https://github.com/alleslabs/celatone-frontend/pull/362) Fix missed out hard-coded osmosis testnet 4 in code +- [#361](https://github.com/alleslabs/celatone-frontend/pull/361) Fix incorrect assigned message index for each pool message in pool tx tables +- [#360](https://github.com/alleslabs/celatone-frontend/pull/360) Fix supported pool list cannot be searched with token ID and should not show pagination when data is less than 10 +- [#403](https://github.com/alleslabs/celatone-frontend/pull/403) Retrieve faucet information from api and use api instead of lcd to prevent CORS +- [#400](https://github.com/alleslabs/celatone-frontend/pull/400) Fallback explorer link for validator/proposal and gov params token symbol +- [#395](https://github.com/alleslabs/celatone-frontend/pull/395) Disable wasm related tabs on the account detail page +- [#392](https://github.com/alleslabs/celatone-frontend/pull/392) Fix format denom function +- [#390](https://github.com/alleslabs/celatone-frontend/pull/390) Fix minor styling +- [#391](https://github.com/alleslabs/celatone-frontend/pull/391) Fix incorrect empty state for past txs table +- [#383](https://github.com/alleslabs/celatone-frontend/pull/383) Fix title input field and navigation in sticky bar in proposal to store code, redirect path for wasm flag +- [#379](https://github.com/alleslabs/celatone-frontend/pull/379) Able to access txs tab when count query timeout, change tabs to lazy load mode for better performance +- [#387](https://github.com/alleslabs/celatone-frontend/pull/387) Fix reseting proposal table when applying filters +- [#375](https://github.com/alleslabs/celatone-frontend/pull/375) Fix incorrect display resolve height in proposals page +- [#356](https://github.com/alleslabs/celatone-frontend/pull/356) Fix store code out of gas error by gzipping file before submitting tx +- [#344](https://github.com/alleslabs/celatone-frontend/pull/344) Fix enable tx bug in proposal to store code + ## v1.0.5 ### Features @@ -47,6 +143,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements +- [#364](https://github.com/alleslabs/celatone-frontend/pull/364) (Contract Details) use instantiated height and time from indexer as fallback +- [#323](https://github.com/alleslabs/celatone-frontend/pull/323) Revise back button and breadcrumb components - [#339](https://github.com/alleslabs/celatone-frontend/pull/339) Update all routes to plural form, and patch all codes - [#334](https://github.com/alleslabs/celatone-frontend/pull/334) Change `osmo-test-4` to `osmo-test-5`, fix tx service when accountId is undefined - [#311](https://github.com/alleslabs/celatone-frontend/pull/311) Refine css styling @@ -55,6 +153,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes +- [#370](https://github.com/alleslabs/celatone-frontend/pull/370) Fix ContractResponse type - [$348](https://github.com/alleslabs/celatone-frontend/pull/348) Workaround for the issue that walletManager local storage is not cleared when switching networks - [$340](https://github.com/alleslabs/celatone-frontend/pull/340) Remove resend and redo button in accordion if relation is related (Past txs page) - [#337](https://github.com/alleslabs/celatone-frontend/pull/337) Fix beforeunload keep showing up Leave modal diff --git a/README.md b/README.md index 44f5ecda0..70eea40d1 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,11 @@ The Celatone frontend uses the following technologies: ### Prerequisites -1. [Node.js](https://nodejs.org/en/) (version >= 14) or nvm installed. -2. [`Yarn`](https://yarnpkg.com/) installed. +1. [Node.js](https://nodejs.org/en/) (version >= 16) or using node version manager [nvm](https://github.com/nvm-sh/nvm#intro) (recommended, installation guide for nvm [here](https://collabnix.com/how-to-install-and-configure-nvm-on-mac-os/)). +2. [`Yarn`](https://yarnpkg.com/getting-started/install) installed. +```bash +npm install -g yarn +``` ### Develop @@ -41,7 +44,15 @@ cd celatone-frontend yarn ``` -3. Finally, run the development server +3. Create a `.env.local` file in the root of the project and add the following environment variables + +```bash +# The mnemonic of the wallet that will be used for estimate gas fees +NEXT_PUBLIC_DUMMY_MNEMONIC="your mnemonic here" +NEXT_PUBLIC_SUPPORTED_CHAIN_IDS=osmosis-1,osmo-test-5 +``` + +4. Finally, run the development server ```bash yarn dev diff --git a/codegen.ts b/codegen.ts index 41900b584..e700ca716 100644 --- a/codegen.ts +++ b/codegen.ts @@ -1,7 +1,10 @@ import type { CodegenConfig } from "@graphql-codegen/cli"; +/** + * @remarks Update schema when it is needed + */ const config: CodegenConfig = { - schema: "https://osmosis-testnet-graphql.alleslabs.dev/v1/graphql", + schema: "https://osmo-test-5-graphql.alleslabs.dev/v1/graphql", documents: ["src/**/*.tsx", "src/**/*.ts"], ignoreNoDocuments: true, // for better experience with the watcher generates: { diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..b53ca008a --- /dev/null +++ b/jest.config.js @@ -0,0 +1,15 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + verbose: true, + preset: "ts-jest", + + moduleDirectories: ["node_modules", "src"], + moduleFileExtensions: ["ts", "tsx", "js", "jsx"], + transform: { + "node_modules/(map-obj|camelcase)/.+\\.(j|t)sx?$": "ts-jest", + }, + testPathIgnorePatterns: ["/node_modules/"], + testRegex: ".test.(tsx?)$", + + transformIgnorePatterns: [`/node_modules/(?!(map-obj|camelcase))`], +}; diff --git a/next.config.js b/next.config.js index be499fb84..c4058f97d 100644 --- a/next.config.js +++ b/next.config.js @@ -45,6 +45,7 @@ const nextConfig = { "contract-list", "past-tx", "my-code", + "pool", ]; return routes.reduce((acc, route) => { @@ -86,4 +87,6 @@ const moduleExports = { }, }; -module.exports = withSentryConfig(moduleExports); +module.exports = withSentryConfig(moduleExports, { + dryRun: process.env.VERCEL_ENV !== "production", +}); diff --git a/package.json b/package.json index bf28f2402..b0eae819b 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "next dev", "build": "next build", + "test": "jest --coverage", "postbuild": "next-sitemap --config next-sitemap.config.js", "start": "next start", "lint": "next lint", @@ -26,15 +27,14 @@ "@chakra-ui/icons": "^2.0.11", "@chakra-ui/react": "^2.3.6", "@chakra-ui/styled-system": "^2.3.5", - "@cosmjs/cosmwasm-stargate": "^0.29.3", - "@cosmjs/encoding": "^0.29.5", - "@cosmjs/proto-signing": "^0.29.5", - "@cosmjs/stargate": "^0.29.3", - "@cosmos-kit/core": "^0.20.0", - "@cosmos-kit/keplr": "^0.20.0", - "@cosmos-kit/react": "^0.19.0", - "@emotion/react": "^11.10.4", - "@emotion/styled": "^11.10.4", + "@cosmjs/cosmwasm-stargate": "^0.30.1", + "@cosmjs/crypto": "^0.30.1", + "@cosmjs/encoding": "^0.30.1", + "@cosmjs/proto-signing": "^0.30.1", + "@cosmjs/stargate": "^0.30.1", + "@cosmos-kit/core": "^1.5.8", + "@cosmos-kit/keplr": "^0.33.38", + "@cosmos-kit/react": "^1.3.31", "@graphql-codegen/cli": "^2.13.12", "@graphql-codegen/client-preset": "^1.1.4", "@rx-stream/pipe": "^0.7.1", @@ -47,7 +47,7 @@ "axios": "^1.1.3", "big.js": "^6.2.1", "camelcase": "^7.0.0", - "chain-registry": "1.13.0", + "chain-registry": "^1.14.0", "cosmjs-types": "^0.7.2", "dayjs": "^1.11.6", "framer-motion": "^7.6.12", @@ -63,6 +63,8 @@ "mobx-react-lite": "^3.4.0", "next": "^13.0.0", "next-seo": "^5.8.0", + "node-gzip": "^1.1.2", + "plur": "^5.1.0", "react": "^18.2.0", "react-ace": "^10.1.0", "react-dom": "^18.2.0", @@ -79,6 +81,8 @@ "devDependencies": { "@commitlint/config-conventional": "^17.1.0", "@commitlint/cz-commitlint": "^17.1.2", + "@types/jest": "^29.4.0", + "@types/node-gzip": "^1.1.0", "@types/react": "^18.0.21", "@types/react-linkify": "^1.0.1", "@types/uuid": "^9.0.0", @@ -89,10 +93,14 @@ "eslint-config-next": "^13.0.0", "eslint-config-sznm": "^1.0.0", "husky": "^8.0.1", + "jest": "^29.5.0", "lint-staged": "^13.0.3", + "mockdate": "^3.0.5", "next-sitemap": "^3.1.25", "prettier": "^2.7.1", "standard-version": "^9.5.0", - "typescript": "^4.8.4" - } + "ts-jest": "^29.0.5", + "typescript": "^4.9.5" + }, + "packageManager": "yarn@1.22.19" } diff --git a/public/celatone-logo.svg b/public/celatone-logo.svg deleted file mode 100644 index dd5dd1627..000000000 --- a/public/celatone-logo.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 1c33309ea..000000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 000000000..5743858d5 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,287 @@ +import type { ContractAddr, HumanAddr, ValidatorAddr } from "lib/types"; + +import type { ChainConfig, ChainConfigs, ProjectConstants } from "./types"; + +export const DEFAULT_CHAIN_CONFIG: ChainConfig = { + chain: "", + registryChainName: "", + prettyName: "", + lcd: "", + rpc: "", + indexer: "", + api: "", + features: { + faucet: { + enabled: false, + }, + wasm: { + enabled: false, + }, + pool: { + enabled: false, + }, + publicProject: { + enabled: false, + }, + }, + gas: { + gasPrice: { + tokenPerGas: 0, + denom: "", + }, + gasAdjustment: 1.0, + maxGasLimit: 0, + }, + exampleAddresses: { + user: "" as HumanAddr, + contract: "" as ContractAddr, + validator: "" as ValidatorAddr, + }, + explorerLink: { + validator: "", + proposal: "", + }, + hasSubHeader: false, +}; + +const DEFAULT_CELATONE_API_ENDPOINT = "https://celatone-api.alleslabs.dev"; + +export const CHAIN_CONFIGS: ChainConfigs = { + "osmosis-1": { + chain: "osmosis", + registryChainName: "osmosis", + prettyName: "Osmosis", + lcd: "https://lcd.osmosis.zone", + rpc: "https://rpc.osmosis.zone", + indexer: "https://osmosis-mainnet-graphql.alleslabs.dev/v1/graphql", + api: DEFAULT_CELATONE_API_ENDPOINT, + features: { + faucet: { + enabled: false, + }, + wasm: { + enabled: true, + storeCodeMaxFileSize: 800_000, + clearAdminGas: 50_000, + }, + pool: { + enabled: true, + url: "https://app.osmosis.zone/pool", + }, + publicProject: { + enabled: true, + }, + }, + gas: { + gasPrice: { + tokenPerGas: 0.025, + denom: "uosmo", + }, + gasAdjustment: 1.5, + maxGasLimit: 25_000_000, + }, + exampleAddresses: { + user: "osmo14wk9zecqam9jsac7xwtf8e349ckquzzlx9k8c3" as HumanAddr, + contract: + "osmo1p0pxllmqjgl2tefy7grypt34jdpdltg3ka98n8unnl322wqps7lqtu576h" as ContractAddr, + validator: + "osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26" as ValidatorAddr, + }, + explorerLink: { + validator: "https://www.mintscan.io/osmosis/validators", + proposal: "https://www.mintscan.io/osmosis/proposals", + }, + hasSubHeader: false, + }, + "osmo-test-5": { + chain: "osmosis", + registryChainName: "osmosistestnet5", + prettyName: "Osmosis Testnet", + lcd: "https://lcd.osmotest5.osmosis.zone", + rpc: "https://rpc.osmotest5.osmosis.zone", + indexer: "https://osmo-test-5-graphql.alleslabs.dev/v1/graphql", + api: DEFAULT_CELATONE_API_ENDPOINT, + features: { + faucet: { + enabled: true, + url: "https://faucet.alleslabs.dev", + }, + wasm: { + enabled: true, + storeCodeMaxFileSize: 800_000, + clearAdminGas: 50_000, + }, + pool: { + enabled: true, + url: "https://testnet.osmosis.zone/pool", + }, + publicProject: { + enabled: false, + }, + }, + gas: { + gasPrice: { + tokenPerGas: 0.025, + denom: "uosmo", + }, + gasAdjustment: 1.5, + maxGasLimit: 25_000_000, + }, + exampleAddresses: { + user: "osmo14wk9zecqam9jsac7xwtf8e349ckquzzlx9k8c3" as HumanAddr, + contract: + "osmo1p0pxllmqjgl2tefy7grypt34jdpdltg3ka98n8unnl322wqps7lqtu576h" as ContractAddr, + validator: + "osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26" as ValidatorAddr, + }, + explorerLink: { + validator: "https://testnet.mintscan.io/osmosis-testnet/validators", + proposal: "https://testnet.mintscan.io/osmosis-testnet/proposals", + }, + hasSubHeader: false, + }, + "pacific-1": { + chain: "sei", + registryChainName: "sei", + prettyName: "Sei", + lcd: "https://sei-api.polkachu.com", + rpc: "https://sei-rpc.polkachu.com", + indexer: "https://pacific-1-graphql.alleslabs.dev/v1/graphql", + api: DEFAULT_CELATONE_API_ENDPOINT, + features: { + faucet: { + enabled: false, + }, + wasm: { + enabled: true, + storeCodeMaxFileSize: 800_000, + clearAdminGas: 50_000, + }, + pool: { + enabled: false, + }, + publicProject: { + enabled: true, + }, + }, + gas: { + gasPrice: { + tokenPerGas: 0.025, + denom: "usei", + }, + gasAdjustment: 1.5, + maxGasLimit: 25_000_000, + }, + exampleAddresses: { + user: "sei1acqpnvg2t4wmqfdv8hq47d3petfksjs5xjfnyj" as HumanAddr, + contract: + "sei18l6zzyyhrl7j9zw2lew50677va25rtsa2s4yy5gdpg4nxz3y3j9se47f0k" as ContractAddr, + validator: + "seivaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26" as ValidatorAddr, + }, + explorerLink: { + validator: "", + proposal: "", + }, + hasSubHeader: true, + }, + "atlantic-2": { + chain: "sei", + registryChainName: "seitestnet2", + prettyName: "Sei Testnet2", + lcd: "https://rest.atlantic-2.seinetwork.io", + rpc: "https://rpc.atlantic-2.seinetwork.io", + indexer: "https://atlantic-2-graphql.alleslabs.dev/v1/graphql", + api: DEFAULT_CELATONE_API_ENDPOINT, + features: { + faucet: { + enabled: false, + }, + wasm: { + enabled: true, + storeCodeMaxFileSize: 800_000, + clearAdminGas: 50_000, + }, + pool: { + enabled: false, + }, + publicProject: { + enabled: false, + }, + }, + gas: { + gasPrice: { + tokenPerGas: 0.025, + denom: "usei", + }, + gasAdjustment: 1.5, + maxGasLimit: 25_000_000, + }, + exampleAddresses: { + user: "sei1acqpnvg2t4wmqfdv8hq47d3petfksjs5xjfnyj" as HumanAddr, + contract: + "sei18l6zzyyhrl7j9zw2lew50677va25rtsa2s4yy5gdpg4nxz3y3j9se47f0k" as ContractAddr, + validator: + "seivaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26" as ValidatorAddr, + }, + explorerLink: { + validator: "https://testnet.mintscan.io/sei-testnet/validators", + proposal: "https://testnet.mintscan.io/sei-testnet/proposals", + }, + hasSubHeader: true, + }, + localosmosis: { + chain: "osmosis", + registryChainName: "localosmosis", + prettyName: "Local Osmosis", + lcd: "http://localhost/rest", + rpc: "http://localhost/rpc/", + indexer: "http://localhost/hasura/v1/graphql", + api: "http://localhost/api", + features: { + faucet: { + enabled: true, + url: "http://localhost:5005/request", + }, + wasm: { + enabled: true, + storeCodeMaxFileSize: 800_000, + clearAdminGas: 50_000, + }, + pool: { + enabled: false, + }, + publicProject: { + enabled: false, + }, + }, + gas: { + gasPrice: { + tokenPerGas: 0.25, + denom: "uosmo", + }, + gasAdjustment: 1.5, + maxGasLimit: 25_000_000, + }, + exampleAddresses: { + user: "osmo14wk9zecqam9jsac7xwtf8e349ckquzzlx9k8c3" as HumanAddr, + contract: + "osmo1p0pxllmqjgl2tefy7grypt34jdpdltg3ka98n8unnl322wqps7lqtu576h" as ContractAddr, + validator: + "osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26" as ValidatorAddr, + }, + explorerLink: { + validator: "", + proposal: "", + }, + hasSubHeader: false, + }, +}; + +export const PROJECT_CONSTANTS: ProjectConstants = { + maxListNameLength: 50, + maxContractNameLength: 50, + maxContractDescriptionLength: 250, + maxCodeNameLength: 50, + maxProposalTitleLength: 255, +}; diff --git a/src/config/theme/default.ts b/src/config/theme/default.ts new file mode 100644 index 000000000..0712e2671 --- /dev/null +++ b/src/config/theme/default.ts @@ -0,0 +1,128 @@ +import type { ThemeConfig } from "./types"; + +export const DEFAULT_THEME: ThemeConfig = { + branding: { + logo: "https://assets.alleslabs.dev/branding/logo/logo.svg", + favicon: "https://assets.alleslabs.dev/branding/favicon.ico", + seo: { + appName: "Celatone", + title: "Celatone Explorer for Cosmos chain", + description: "A smart contract powered explorer for the Cosmos.", + image: "https://assets.alleslabs.dev/branding/celatone-cover.jpg", + twitter: { + handle: "@celatone_", + cardType: "summary_large_image", + }, + }, + }, + fonts: { + heading: { + url: "https://fonts.googleapis.com/css2?family=Poppins:wght@500;600&display=swap", + name: "Poppins, serif", + }, + body: { + url: "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap", + name: "Space Grotesk, sans-serif", + }, + }, + colors: { + gradient: { + main: "linear(to-tr, #5942F3, #9793F3)", + }, + error: { + main: "#FF666E", + light: "#FF8086", + dark: "#B43E44", + background: "#4C1A1D", + }, + warning: { + main: "#FFBB33", + light: "#FFCC66", + dark: "#CC8800", + background: "#523600", + }, + success: { + main: "#42BEA6", + light: "#67CBB7", + dark: "#207966", + background: "#102E28", + }, + background: { + main: "#111117", + overlay: "rgba(17, 17, 23, 0.7)", + }, + text: { + main: "#F7F2FE", + dark: "#ADADC2", + disabled: "#8A8AA5", + }, + primary: { + main: "#5942F3", + light: "#9793F3", + lighter: "#DCDBFB", + dark: "#3E38B0", + darker: "#292676", + background: "#181733", + }, + secondary: { + main: "#D8BEFC", + light: "#E8D8FD", + dark: "#A28FBD", + darker: "#6C5F7E", + background: "#36303F", + }, + accent: { + main: "#C6E141", + light: "#DDED8D", + lighter: "#E8F3B3", + dark: "#95A931", + darker: "#637121", + background: "#3D470B", + }, + gray: { + 100: "#F7F2FE", + 400: "#ADADC2", + 500: "#8A8AA5", + 600: "#68688A", + 700: "#343445", + 800: "#272734", + 900: "#1A1A22", + }, + }, + tag: { + signer: { + bg: "accent.darker", + color: "inherit", + }, + related: { + bg: "primary.dark", + color: "text.main", + }, + }, + borderRadius: { + default: "8px", + iconButton: "36px", + viewButton: "0 0 8px 8px", + uploadButton: "50%", + tag: "full", + badge: "16px", + radio: "12px", + indicator: "2px", + stepper: "full", + }, + jsonTheme: "monokai", + illustration: { + error: "https://assets.alleslabs.dev/illustration/404.svg", + searchNotFound: + "https://assets.alleslabs.dev/illustration/search-not-found.svg", + searchEmpty: "https://assets.alleslabs.dev/illustration/search-empty.svg", + disconnected: "https://assets.alleslabs.dev/illustration/disconnected.svg", + }, + socialMedia: { + website: "https://celat.one/", + github: "https://github.com/alleslabs", + twitter: "https://twitter.com/celatone_", + medium: "https://blog.alleslabs.com/", + telegram: "https://t.me/celatone_announcements", + }, +}; diff --git a/src/config/theme/index.ts b/src/config/theme/index.ts new file mode 100644 index 000000000..5c652a933 --- /dev/null +++ b/src/config/theme/index.ts @@ -0,0 +1,3 @@ +export * from "./default"; +export * from "./osmosis"; +export * from "./sei"; diff --git a/src/config/theme/osmosis.ts b/src/config/theme/osmosis.ts new file mode 100644 index 000000000..f10afd502 --- /dev/null +++ b/src/config/theme/osmosis.ts @@ -0,0 +1,141 @@ +import type { ThemeConfig } from "./types"; + +export const OSMOSIS_THEME: ThemeConfig = { + branding: { + logo: "https://assets.alleslabs.dev/integrations/osmosis/logo.svg", + favicon: "https://assets.alleslabs.dev/integrations/osmosis/favicon.ico", + seo: { + appName: "osmoscan", + title: "OsmoScan powered by Celatone", + description: + "Explore, deploy, execute, and query smart contracts on Osmosis from a user-friendly web UI", + image: + "https://uploads-ssl.webflow.com/62825f7982e99cdbe7e37258/63ffa0f2d03be626855120ae_image-_4_.webp", + twitter: { + handle: "@osmosiszone", + cardType: "summary_large_image", + }, + }, + }, + fonts: { + heading: { + url: "https://fonts.googleapis.com/css2?family=Poppins:wght@500;600&display=swap", + name: "Poppins, serif", + }, + body: { + url: "https://fonts.googleapis.com/css2?family=Inter:wght@300..700&display=swap", + name: "Inter, sans-serif", + }, + }, + colors: { + gradient: { + main: "linear(55deg, #462ADF 0%,#5235EF 40%, #B72AAB 100%)", + }, + error: { + main: "#FF666E", + light: "#FF8086", + dark: "#B43E44", + background: "#4C1A1D", + }, + warning: { + main: "#FFBB33", + light: "#FFCC66", + dark: "#CC8800", + background: "#523600", + }, + success: { + main: "#42BEA6", + light: "#67CBB7", + dark: "#207966", + background: "#102E28", + }, + background: { + main: "#090524", + overlay: "rgba(14, 9, 49, 0.7)", + }, + text: { + main: "#F2F2F4", + dark: "#A09ACA", + disabled: "#736DA0", + }, + primary: { + main: "#5235EF", + light: "#765CFF", + lighter: "#8481F8", + dark: "#462ADF", + darker: "#3A1FCA", + background: "#201865", + }, + secondary: { + main: "#8481F8", + light: "#9B99FF", + dark: "#6A67EA", + darker: "#5855DB", + background: "#2F306A", + }, + accent: { + main: "#DD69D3", + light: "#E58BDD", + lighter: "#ECACE7", + dark: "#CA2EBD", + darker: "#B72AAB", + background: "#431152", + }, + gray: { + 100: "#F2F2F4", + 400: "#8D87B8", + 500: "#736DA0", + 600: "#5F588F", + 700: "#464075", + 800: "#282750", + 900: "#140F34", + }, + }, + tag: { + signer: { + bg: "accent.darker", + color: "inherit", + }, + related: { + bg: "primary.dark", + color: "text.main", + }, + }, + borderRadius: { + default: "8px", + iconButton: "36px", + viewButton: "0 0 8px 8px", + uploadButton: "50%", + tag: "full", + badge: "16px", + radio: "12px", + indicator: "2px", + stepper: "full", + }, + jsonTheme: "pastel_on_dark", + illustration: { + error: + "https://assets.alleslabs.dev/integrations/osmosis/illustration/404.svg", + searchNotFound: + "https://assets.alleslabs.dev/integrations/osmosis/illustration/search-not-found.svg", + searchEmpty: + "https://assets.alleslabs.dev/integrations/osmosis/illustration/search-empty.svg", + disconnected: + "https://assets.alleslabs.dev/integrations/osmosis/illustration/disconnected.svg", + }, + footer: { + logo: "https://assets.alleslabs.dev/integrations/osmosis/logo.png", + description: + "A Smart Contract Explorer for Osmosis | Explore, deploy, execute, and query smart contracts on Osmosis from a user-friendly web UI", + iconStyle: "rounded", + }, + socialMedia: { + website: "https://osmosis.zone/", + github: "https://github.com/osmosis-labs/osmosis", + discord: "https://discord.com/invite/osmosis", + twitter: "https://twitter.com/osmosiszone", + medium: "https://medium.com/osmosis", + telegram: "https://t.me/osmosis_chat", + reddit: "https://www.reddit.com/r/OsmosisLab/", + }, +}; diff --git a/src/config/theme/sei.ts b/src/config/theme/sei.ts new file mode 100644 index 000000000..901dc85eb --- /dev/null +++ b/src/config/theme/sei.ts @@ -0,0 +1,151 @@ +import type { ThemeConfig } from "config/theme/types"; + +export const SEI_THEME: ThemeConfig = { + branding: { + logo: "https://assets.alleslabs.dev/integrations/sei/logo.svg", + favicon: "https://assets.alleslabs.dev/integrations/sei/favicon.ico", + seo: { + appName: "SeiScan", + title: "SeiScan powered by Celatone", + description: + "Explore, deploy, execute, and query smart contracts on Sei from a user-friendly web UI", + image: "https://assets.alleslabs.dev/integrations/sei/cover.jpg", + twitter: { + handle: "@SeiNetwork", + cardType: "summary_large_image", + }, + }, + }, + fonts: { + heading: { + url: "https://fonts.cdnfonts.com/css/satoshi?styles=135009,135005,135007,135002,135000", + name: "Satoshi, sans-serif", + }, + body: { + url: "https://fonts.cdnfonts.com/css/satoshi?styles=135009,135005,135007,135002,135000", + name: "Satoshi, sans-serif", + }, + }, + colors: { + gradient: { + main: "linear(55deg, #1D343F 0%, #184354 100%)", + }, + error: { + main: "#FF666E", + light: "#FF8086", + dark: "#B43E44", + background: "#4C1A1D", + }, + warning: { + main: "#FFBB33", + light: "#FFCC66", + dark: "#CC8800", + background: "#523600", + }, + success: { + main: "#42BEA6", + light: "#67CBB7", + dark: "#207966", + background: "#102E28", + }, + background: { + main: "#000E13", + overlay: "rgba(6, 21, 27, 0.7)", + }, + text: { + main: "#FCFCFD", + dark: "#92A4B5", + disabled: "#586D81", + }, + primary: { + main: "#F0E3CF", + light: "#FAF6EF", + lighter: "#FAF6EF", + dark: "#C0B8A9", + darker: "#787971", + background: "#303939", + }, + secondary: { + main: "#6C80B2", + light: "#8797C0", + dark: "#184354", + darker: "#3F4F78", + background: "#203043", + }, + accent: { + main: "#6C80B2", + light: "#8797C0", + lighter: "#8797C0", + dark: "#516799", + darker: "#3F4F78", + background: "#203043", + }, + gray: { + 100: "#FCFCFD", + 400: "#92A4B5", + 500: "#586D81", + 600: "#40566A", + 700: "#1D343F", + 800: "#132730", + 900: "#0C1C23", + }, + }, + tag: { + signer: { + bg: "accent.darker", + color: "inherit", + }, + related: { + bg: "primary.dark", + color: "gray.900", + }, + }, + button: { + primary: { + background: "#F0E3CF", + color: "#0C1C23", + disabledBackground: "#787971", + disabledColor: "#0C1C23", + }, + outlinePrimary: { + borderColor: "#787971", + color: "#F0E3CF", + disabledBorderColor: "#1D343F", + disabledColor: "#40566A", + }, + }, + borderRadius: { + default: "8px", + iconButton: "36px", + viewButton: "0 0 8px 8px", + uploadButton: "50%", + tag: "full", + badge: "16px", + radio: "12px", + indicator: "2px", + stepper: "full", + }, + jsonTheme: "one_dark", + illustration: { + error: "https://assets.alleslabs.dev/integrations/sei/illustration/404.svg", + searchNotFound: + "https://assets.alleslabs.dev/integrations/sei/illustration/search-not-found.svg", + searchEmpty: + "https://assets.alleslabs.dev/integrations/sei/illustration/search-empty.svg", + disconnected: + "https://assets.alleslabs.dev/integrations/sei/illustration/disconnected.svg", + }, + footer: { + logo: "https://www.sei.io/_next/static/media/logo-light.1249fa55.svg", + description: + "A Smart Contract Explorer for Sei | Explore, deploy, execute, and query smart contracts on Sei from a user-friendly web UI", + iconStyle: "rounded", + }, + socialMedia: { + website: "https://www.sei.io/", + github: "https://github.com/sei-protocol/sei-chain", + discord: "https://discord.com/invite/sei", + twitter: "https://twitter.com/SeiNetwork", + telegram: "https://t.me/seinetwork", + }, +}; diff --git a/src/config/theme/types.ts b/src/config/theme/types.ts new file mode 100644 index 000000000..f09d4403f --- /dev/null +++ b/src/config/theme/types.ts @@ -0,0 +1,147 @@ +export type ThemeConfig = { + branding: { + logo: string; + favicon: string; + seo: { + appName: string; + title: string; + description: string; + image: string; + twitter: { + handle: string; + cardType: string; + }; + }; + }; + fonts: { + heading: { + url: string; + name: string; + }; + body: { + url: string; + name: string; + }; + }; + colors: { + gradient?: { + main: string; + }; + error: { + main: string; + light: string; + dark: string; + background: string; + }; + warning: { + main: string; + light: string; + dark: string; + background: string; + }; + success: { + main: string; + light: string; + dark: string; + background: string; + }; + background: { + main: string; + overlay: string; + }; + text: { + main: string; + dark: string; + disabled: string; + }; + primary: { + main: string; + light: string; + lighter: string; + dark: string; + darker: string; + background: string; + }; + secondary: { + main: string; + light: string; + dark: string; + darker: string; + background: string; + }; + accent: { + main: string; + light: string; + lighter: string; + dark: string; + darker: string; + background: string; + }; + gray: { + 100: string; + 400: string; + 500: string; + 600: string; + 700: string; + 800: string; + 900: string; + }; + }; + tag: { + signer: { + bg: string; + color: string; + }; + related: { + bg: string; + color: string; + }; + }; + button?: { + primary?: { + background: string; + color: string; + disabledBackground: string; + disabledColor: string; + }; + outlinePrimary?: { + borderColor: string; + color: string; + disabledBorderColor: string; + disabledColor: string; + }; + }; + borderRadius: { + default: string; + iconButton: string; + viewButton: string; + uploadButton: string; + tag: string; + badge: string; + radio: string; + indicator: string; + stepper: string; + }; + illustration: { + error: string; + searchNotFound: string; + searchEmpty: string; + disconnected: string; + }; + jsonTheme: "monokai" | "one_dark" | "pastel_on_dark"; + footer?: { + logo: string; + description: string; + iconStyle: "rounded" | "regular"; + }; + socialMedia?: { + website?: string; + github?: string; + discord?: string; + twitter?: string; + medium?: string; + telegram?: string; + reddit?: string; + linkedin?: string; + }; +}; diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 000000000..87ed0c9dc --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,78 @@ +import type { HumanAddr, ValidatorAddr, ContractAddr } from "lib/types"; + +type FaucetConfig = + | { + enabled: true; + url: string; + } + | { enabled: false }; + +type WasmConfig = + | { + enabled: true; + storeCodeMaxFileSize: number; + clearAdminGas: number; + } + | { + enabled: false; + }; + +type PoolConfig = + | { + enabled: true; + url: string; + } + | { enabled: false }; + +type PublicProjectConfig = { enabled: boolean }; + +export interface ExplorerConfig { + validator: string; + proposal: string; +} + +export interface ChainConfig { + chain: string; + registryChainName: string; + prettyName: string; + lcd: string; + rpc: string; + indexer: string; + api: string; + features: { + faucet: FaucetConfig; + wasm: WasmConfig; + pool: PoolConfig; + publicProject: PublicProjectConfig; + }; + gas: { + gasPrice: { + tokenPerGas: number; + denom: string; + }; + gasAdjustment: number; + maxGasLimit: number; + }; + exampleAddresses: { + user: HumanAddr; + validator: ValidatorAddr; + contract: ContractAddr; + }; + explorerLink: ExplorerConfig; + hasSubHeader: boolean; +} + +export interface ChainConfigs { + [chainId: string]: ChainConfig; +} + +export interface ProjectConstants { + // wasm + maxListNameLength: number; + maxContractNameLength: number; + maxContractDescriptionLength: number; + maxCodeNameLength: number; + + // proposal + maxProposalTitleLength: number; +} diff --git a/src/env.ts b/src/env.ts index 5aa643efe..2327f6798 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,119 +1,34 @@ -import type { SupportedChain } from "lib/data"; -import type { - ContractAddr, - ChainGasPrice, - Token, - U, - HumanAddr, -} from "lib/types"; -import type { - CelatoneConstants, - CelatoneContractAddress, - CelatoneHumanAddress, -} from "types"; +import { DEFAULT_THEME, OSMOSIS_THEME, SEI_THEME } from "config/theme"; -export const CELATONE_FALLBACK_GAS_PRICE: Record = { - osmosistestnet5: { - denom: "uosmo", - gasPrice: "0.025" as U, - }, - terra2: { - denom: "uluna", - gasPrice: "0.015" as U, - }, - terra2testnet: { - denom: "uluna", - gasPrice: "0.15" as U, - }, -}; +export const SUPPORTED_CHAIN_IDS: string[] = (() => { + const chainIds = process.env.NEXT_PUBLIC_SUPPORTED_CHAIN_IDS?.split(","); + if (!chainIds) + throw new Error("NEXT_PUBLIC_SUPPORTED_CHAIN_IDS is undefined"); -export const CELATONE_APP_CONTRACT_ADDRESS = ( - chainName: string -): CelatoneContractAddress => { - switch (chainName) { - case "osmosis": - case "osmosistestnet5": - return { - example: - "osmo1p0pxllmqjgl2tefy7grypt34jdpdltg3ka98n8unnl322wqps7lqtu576h" as ContractAddr, - }; - case "terra2": - case "terra2testnet": - return { - example: - "terra1k5arpcpusfrtnucr5q8f5uh5twghh3q360hv4j6fe0hvzn7x8skqempu76" as ContractAddr, - }; - default: - return { - example: "" as ContractAddr, - }; - } -}; + if (chainIds[0].trim().length === 0) + throw new Error( + "NEXT_PUBLIC_SUPPORTED_CHAIN_IDS is not valid. Please include at least one chain identifier. For instance, NEXT_PUBLIC_SUPPORTED_CHAIN_IDS=osmo-test-5" + ); -export const CELATONE_APP_HUMAN_ADDRESS = ( - chainName: string -): CelatoneHumanAddress => { - switch (chainName) { - case "osmosis": - case "osmosistestnet5": - return { - example: "osmo14wk9zecqam9jsac7xwtf8e349ckquzzlx9k8c3" as HumanAddr, - }; - case "terra2": - case "terra2testnet": - return { - example: "terra1dtdqq3sn8c6y6sjvtf4340aycv2g6x6pp5tkln" as HumanAddr, - }; - default: - return { - example: "" as HumanAddr, - }; - } -}; + return chainIds; +})(); -export const FALLBACK_LCD_ENDPOINT: Record = { - osmosis: "https://lcd.osmosis.zone/", - osmosistestnet5: "https://lcd.osmotest5.osmosis.zone/", - terra2: "https://phoenix-lcd.terra.dev/", - terra2testnet: "https://pisco-lcd.terra.dev/", -}; - -export const MAX_FILE_SIZE = 800_000; - -export const CELATONE_CONSTANTS: CelatoneConstants = { - gasAdjustment: 1.6, - maxFileSize: MAX_FILE_SIZE, -}; +// Remark: We've already checked that the first element is not empty on the above code +export const DEFAULT_SUPPORTED_CHAIN_ID = SUPPORTED_CHAIN_IDS[0]; export const DUMMY_MNEMONIC = process.env.NEXT_PUBLIC_DUMMY_MNEMONIC; -export const SELECTED_CHAIN = process.env - .NEXT_PUBLIC_SELECTED_CHAIN as SupportedChain; - -export const CELATONE_API_ENDPOINT = "https://celatone-api.alleslabs.dev"; +export const CELATONE_API_OVERRIDE = + process.env.NEXT_PUBLIC_CELATONE_API_OVERRIDE; -export const getChainApiPath = (chainName: string) => { - switch (chainName) { - case "osmosistestnet5": +// CURRENT THEME CONFIG +export const CURR_THEME = (() => { + switch (process.env.NEXT_PUBLIC_THEME) { case "osmosis": - return "osmosis"; - case "terra2": - case "terra2testnet": - return "terra"; - default: - return undefined; - } -}; -// TODO to handle testnet separately later -export const getMainnetApiPath = (chainId: string) => { - switch (chainId) { - case "osmo-test-4": - case "osmosis-1": - return "osmosis-1"; - case "pisco-1": - case "phoenix-1": - return "phoenix-1"; + return OSMOSIS_THEME; + case "sei": + return SEI_THEME; default: - return undefined; + return DEFAULT_THEME; } -}; +})(); diff --git a/src/lib/app-fns/explorer/index.ts b/src/lib/app-fns/explorer/index.ts deleted file mode 100644 index d17f1397d..000000000 --- a/src/lib/app-fns/explorer/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -export const explorerMap: Record = { - osmosis: "https://www.mintscan.io/osmosis", - osmosistestnet5: "https://testnet.mintscan.io/osmosis-testnet", - terra2: "https://terrasco.pe/mainnet", - terra2testnet: "https://terrasco.pe/testnet", -}; - -export const getExplorerProposalUrl = (chainName: string) => { - let pathSuffix = ""; - switch (chainName) { - case "osmosis": - case "osmosistestnet5": - pathSuffix = "proposals"; - break; - case "terra2": - return "https://station.terra.money/proposal/phoenix-1"; - case "terra2testnet": - return "https://station.terra.money/proposal/pisco-1"; - default: - break; - } - return `${explorerMap[chainName]}/${pathSuffix}`; -}; - -export const getExplorerValidatorUrl = (chainName: string) => { - let pathSuffix = ""; - switch (chainName) { - case "osmosis": - case "osmosistestnet5": - pathSuffix = "validators"; - break; - case "terra2": - case "terra2testnet": - pathSuffix = "validator"; - break; - default: - break; - } - return `${explorerMap[chainName]}/${pathSuffix}`; -}; diff --git a/src/lib/app-fns/tx/clearAdmin.tsx b/src/lib/app-fns/tx/clearAdmin.tsx index 2ffd2d9dc..893b259ee 100644 --- a/src/lib/app-fns/tx/clearAdmin.tsx +++ b/src/lib/app-fns/tx/clearAdmin.tsx @@ -70,7 +70,7 @@ export const clearAdminTx = ({ ), }, diff --git a/src/lib/app-fns/tx/common/catchTxError.tsx b/src/lib/app-fns/tx/common/catchTxError.tsx index b5b945a4a..977d08cf0 100644 --- a/src/lib/app-fns/tx/common/catchTxError.tsx +++ b/src/lib/app-fns/tx/common/catchTxError.tsx @@ -36,7 +36,6 @@ export const catchTxError = ( ): OperatorFunction => { return catchError((error: Error) => { const txHash = error.message.match("(?:tx )(.*?)(?= at)")?.at(1); - AmpTrack( error.message === "Request rejected" ? AmpEvent.TX_REJECTED @@ -52,7 +51,7 @@ export const catchTxError = ( ), }, diff --git a/src/lib/app-fns/tx/execute.tsx b/src/lib/app-fns/tx/execute.tsx index 61d491f5f..75cf17da6 100644 --- a/src/lib/app-fns/tx/execute.tsx +++ b/src/lib/app-fns/tx/execute.tsx @@ -88,7 +88,7 @@ export const executeContractTx = ({ ), }, diff --git a/src/lib/app-fns/tx/instantiate.tsx b/src/lib/app-fns/tx/instantiate.tsx index e0ce180ce..83dda08a4 100644 --- a/src/lib/app-fns/tx/instantiate.tsx +++ b/src/lib/app-fns/tx/instantiate.tsx @@ -23,6 +23,7 @@ interface InstantiateTxParams { funds: Coin[]; client: SigningCosmWasmClient; onTxSucceed?: (txInfo: InstantiateResult, contractLabel: string) => void; + onTxFailed?: () => void; } export const instantiateContractTx = ({ @@ -35,6 +36,7 @@ export const instantiateContractTx = ({ funds, client, onTxSucceed, + onTxFailed, }: InstantiateTxParams): Observable => { return pipe( sendingTx(fee), @@ -51,5 +53,5 @@ export const instantiateContractTx = ({ // TODO: this is type hack return null as unknown as TxResultRendering; } - )().pipe(catchTxError()); + )().pipe(catchTxError(onTxFailed)); }; diff --git a/src/lib/app-fns/tx/resend.tsx b/src/lib/app-fns/tx/resend.tsx index a86bf0065..5f5140bd7 100644 --- a/src/lib/app-fns/tx/resend.tsx +++ b/src/lib/app-fns/tx/resend.tsx @@ -64,7 +64,7 @@ export const resendTx = ({ receiptInfo: { header: "Transaction Completed", description: ( - + Your transaction was successfully resent. ), @@ -72,7 +72,7 @@ export const resendTx = ({ ), }, diff --git a/src/lib/app-fns/tx/submitProposal.tsx b/src/lib/app-fns/tx/submitProposal.tsx index 95c14a0d8..22847dc75 100644 --- a/src/lib/app-fns/tx/submitProposal.tsx +++ b/src/lib/app-fns/tx/submitProposal.tsx @@ -9,11 +9,11 @@ import { CustomIcon } from "lib/components/icon"; import { AmpEvent, AmpTrack } from "lib/services/amplitude"; import type { HumanAddr, TxResultRendering } from "lib/types"; import { TxStreamPhase } from "lib/types"; -import { findAttr, formatUFee } from "lib/utils"; +import { capitalize, findAttr, formatUFee } from "lib/utils"; import { catchTxError, postTx, sendingTx } from "./common"; -interface SubmitProposalTxParams { +interface SubmitWhitelistProposalTxParams { address: HumanAddr; client: SigningCosmWasmClient; onTxSucceed?: () => void; @@ -24,7 +24,7 @@ interface SubmitProposalTxParams { amountToVote: string | null; } -export const submitProposalTx = ({ +export const submitWhitelistProposalTx = ({ address, client, onTxSucceed, @@ -33,7 +33,7 @@ export const submitProposalTx = ({ messages, whitelistNumber, amountToVote, -}: SubmitProposalTxParams): Observable => { +}: SubmitWhitelistProposalTxParams): Observable => { return pipe( sendingTx(fee), postTx({ @@ -89,7 +89,88 @@ export const submitProposalTx = ({ headerIcon: ( + ), + }, + actionVariant: "proposal", + } as TxResultRendering; + } + )().pipe(catchTxError(onTxFailed)); +}; + +interface SubmitStoreCodeProposalTxParams { + address: HumanAddr; + client: SigningCosmWasmClient; + fee: StdFee; + chainName: string; + wasmFileName: string; + messages: EncodeObject[]; + amountToVote: string | null; + onTxSucceed?: () => void; + onTxFailed?: () => void; +} + +export const submitStoreCodeProposalTx = ({ + address, + client, + fee, + chainName, + wasmFileName, + messages, + amountToVote, + onTxSucceed, + onTxFailed, +}: SubmitStoreCodeProposalTxParams): Observable => { + return pipe( + sendingTx(fee), + postTx({ + postFn: () => client.signAndBroadcast(address, messages, fee), + }), + ({ value: txInfo }) => { + AmpTrack(AmpEvent.TX_SUCCEED); + onTxSucceed?.(); + const mimicLog: logs.Log = { + msg_index: 0, + log: "", + events: txInfo.events, + }; + const txFee = findAttr(mimicLog, "tx", "fee"); + const proposalId = + findAttr(mimicLog, "submit_proposal", "proposal_id") ?? ""; + return { + value: null, + phase: TxStreamPhase.SUCCEED, + receipts: [ + { + title: "Proposal ID", + value: proposalId, + html: , + }, + { + title: "Tx Hash", + value: txInfo.transactionHash, + html: ( + + ), + }, + { + title: "Tx Fee", + value: txFee ? formatUFee(txFee) : "N/A", + }, + ], + receiptInfo: { + header: "Proposal Submitted", + description: `${wasmFileName} is uploaded and pending ${ + amountToVote + ? ` minimum deposit of ${amountToVote} to trigger voting period.` + : ` ${capitalize(chainName)} governance voting.` + }`, + headerIcon: ( + ), diff --git a/src/lib/app-fns/tx/upload.tsx b/src/lib/app-fns/tx/upload.tsx index 423d72831..8d84d3b28 100644 --- a/src/lib/app-fns/tx/upload.tsx +++ b/src/lib/app-fns/tx/upload.tsx @@ -1,8 +1,5 @@ -import type { - SigningCosmWasmClient, - UploadResult, -} from "@cosmjs/cosmwasm-stargate"; -import type { StdFee } from "@cosmjs/stargate"; +import type { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import type { DeliverTxResponse, logs, StdFee } from "@cosmjs/stargate"; import { pipe } from "@rx-stream/pipe"; import type { Observable } from "rxjs"; @@ -10,7 +7,8 @@ import { ExplorerLink } from "lib/components/ExplorerLink"; import { CustomIcon } from "lib/components/icon"; import { AmpEvent, AmpTrack } from "lib/services/amplitude"; import { TxStreamPhase } from "lib/types"; -import type { HumanAddr, TxResultRendering } from "lib/types"; +import type { HumanAddr, TxResultRendering, ComposedMsg } from "lib/types"; +import { findAttr } from "lib/utils"; import { formatUFee } from "lib/utils/formatter/denom"; import { catchTxError } from "./common/catchTxError"; @@ -19,8 +17,8 @@ import { sendingTx } from "./common/sending"; interface UploadTxParams { address: HumanAddr; - codeDesc: string; - wasmCode: Uint8Array; + codeName: string; + messages: ComposedMsg[]; wasmFileName: string; fee: StdFee; memo?: string; @@ -31,8 +29,8 @@ interface UploadTxParams { export const uploadContractTx = ({ address, - codeDesc, - wasmCode, + codeName, + messages, wasmFileName, fee, memo, @@ -42,12 +40,20 @@ export const uploadContractTx = ({ }: UploadTxParams): Observable => { return pipe( sendingTx(fee), - postTx({ - postFn: () => client.upload(address, wasmCode, fee, memo), + postTx({ + postFn: () => client.signAndBroadcast(address, messages, fee, memo), }), ({ value: txInfo }) => { AmpTrack(AmpEvent.TX_SUCCEED); - onTxSucceed?.(txInfo.codeId); + const mimicLog: logs.Log = { + msg_index: 0, + log: "", + events: txInfo.events, + }; + + const codeId = findAttr(mimicLog, "store_code", "code_id") ?? "0"; + + onTxSucceed?.(parseInt(codeId, 10)); const txFee = txInfo.events.find((e) => e.type === "tx")?.attributes[0] .value; return { @@ -56,14 +62,10 @@ export const uploadContractTx = ({ receipts: [ { title: "Code ID", - value: txInfo.codeId, + value: codeId, html: (
- +
), }, @@ -88,14 +90,14 @@ export const uploadContractTx = ({ description: ( <> - ‘{codeDesc || `${wasmFileName}(${txInfo.codeId})`}’ + ‘{codeName || `${wasmFileName}(${codeId})`}’ {" "} is has been uploaded. Would you like to{" "} {isMigrate ? "migrate" : "instantiate"} your code now? ), headerIcon: ( - + ), }, actionVariant: isMigrate ? "upload-migrate" : "upload", diff --git a/src/lib/app-provider/contexts/app.tsx b/src/lib/app-provider/contexts/app.tsx index f76604bae..f4af70003 100644 --- a/src/lib/app-provider/contexts/app.tsx +++ b/src/lib/app-provider/contexts/app.tsx @@ -1,156 +1,122 @@ -import { useWallet } from "@cosmos-kit/react"; -import big from "big.js"; +import { useModalTheme } from "@cosmos-kit/react"; import { GraphQLClient } from "graphql-request"; import { observer } from "mobx-react-lite"; import type { ReactNode } from "react"; -import { useEffect, useContext, useMemo, createContext } from "react"; +import { + useCallback, + useState, + useEffect, + useContext, + useMemo, + createContext, +} from "react"; import { useAmplitude } from "../hooks/useAmplitude"; import { useNetworkChange } from "../hooks/useNetworkChange"; -import { getIndexerGraphClient } from "../query-client"; -import type { AppConstants } from "../types"; +import { CHAIN_CONFIGS, DEFAULT_CHAIN_CONFIG, PROJECT_CONSTANTS } from "config"; +import type { ChainConfig, ProjectConstants } from "config/types"; +import { SUPPORTED_CHAIN_IDS } from "env"; import { LoadingOverlay } from "lib/components/LoadingOverlay"; +import { NetworkErrorState } from "lib/components/state/NetworkErrorState"; import { DEFAULT_ADDRESS } from "lib/data"; import { useCodeStore, useContractStore, usePublicProjectStore, } from "lib/providers/store"; -import type { ChainGasPrice, Token, U } from "lib/types"; import { formatUserKey } from "lib/utils"; -interface AppProviderProps< - AppContractAddress, - AppHumanAddress, - Constants extends AppConstants -> { +interface AppProviderProps { children: ReactNode; - - fallbackGasPrice: Record; - - appContractAddressMap: (currentChainName: string) => AppContractAddress; - appHumanAddressMap: (currentChainName: string) => AppHumanAddress; - - constants: Constants; } -interface AppContextInterface< - ContractAddress, - HumanAddress, - Constants extends AppConstants = AppConstants -> { - chainGasPrice: ChainGasPrice; - appContractAddress: ContractAddress; - appHumanAddress: HumanAddress; - constants: Constants; +interface AppContextInterface { + availableChainIds: string[]; + currentChainId: string; + chainConfig: ChainConfig; indexerGraphClient: GraphQLClient; + constants: ProjectConstants; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const AppContext = createContext>({ - chainGasPrice: { denom: "", gasPrice: "0" as U }, - appContractAddress: {}, - appHumanAddress: {}, - constants: { gasAdjustment: 0 }, - indexerGraphClient: new GraphQLClient(""), +const AppContext = createContext({ + availableChainIds: [], + currentChainId: "", + chainConfig: DEFAULT_CHAIN_CONFIG, + indexerGraphClient: new GraphQLClient(DEFAULT_CHAIN_CONFIG.indexer), + constants: PROJECT_CONSTANTS, +}); + +export const AppProvider = observer(({ children }: AppProviderProps) => { + const { setCodeUserKey, isCodeUserKeyExist } = useCodeStore(); + const { setContractUserKey, isContractUserKeyExist } = useContractStore(); + const { setProjectUserKey, isProjectUserKeyExist } = usePublicProjectStore(); + const { setModalTheme } = useModalTheme(); + + const [currentChainName, setCurrentChainName] = useState(); + const [currentChainId, setCurrentChainId] = useState(""); + + // Remark: this function is only used in useSelectChain. Do not use in other places. + const handleOnChainIdChange = useCallback((newChainId: string) => { + const config = CHAIN_CONFIGS[newChainId]; + setCurrentChainId(newChainId); + setCurrentChainName(config?.registryChainName); + }, []); + + const states = useMemo(() => { + const chainConfig = CHAIN_CONFIGS[currentChainId] ?? DEFAULT_CHAIN_CONFIG; + + return { + availableChainIds: SUPPORTED_CHAIN_IDS, + currentChainId, + chainConfig, + indexerGraphClient: new GraphQLClient(chainConfig.indexer), + constants: PROJECT_CONSTANTS, + }; + }, [currentChainId]); + + useEffect(() => { + if (currentChainName) { + const userKey = formatUserKey(currentChainName, DEFAULT_ADDRESS); + setCodeUserKey(userKey); + setContractUserKey(userKey); + setProjectUserKey(userKey); + } + }, [currentChainName, setCodeUserKey, setContractUserKey, setProjectUserKey]); + + // Disable "Leave page" alert + useEffect(() => { + const handler = (e: BeforeUnloadEvent) => { + e.stopImmediatePropagation(); + }; + + window.addEventListener("beforeunload", handler); + return () => window.removeEventListener("beforeunload", handler); + }, []); + + useEffect(() => { + if (localStorage.getItem("cosmology-ui-theme") !== "dark") { + setModalTheme("dark"); + } + }, [setModalTheme]); + + useNetworkChange(handleOnChainIdChange); + + useAmplitude(currentChainName); + + if (currentChainId && !(currentChainId in CHAIN_CONFIGS)) + return ; + + if ( + !isCodeUserKeyExist() || + !isContractUserKeyExist() || + !isProjectUserKeyExist() || + !currentChainId + ) + return ; + + return {children}; }); -export const AppProvider = observer( - ({ - children, - fallbackGasPrice, - appContractAddressMap, - appHumanAddressMap, - constants, - }: AppProviderProps) => { - const { currentChainName, currentChainRecord } = useWallet(); - const { setCodeUserKey, isCodeUserKeyExist } = useCodeStore(); - const { setContractUserKey, isContractUserKeyExist } = useContractStore(); - const { setProjectUserKey, isProjectUserKeyExist } = - usePublicProjectStore(); - - useEffect(() => { - const handler = (e: BeforeUnloadEvent) => { - e.stopImmediatePropagation(); - }; - - window.addEventListener("beforeunload", handler); - return () => window.removeEventListener("beforeunload", handler); - }, []); - - const chainGasPrice = useMemo(() => { - if ( - !currentChainRecord || - !currentChainRecord.chain.fees || - !currentChainRecord.chain.fees.fee_tokens[0].average_gas_price - ) - return fallbackGasPrice[currentChainName]; - return { - denom: currentChainRecord.chain.fees?.fee_tokens[0].denom as string, - gasPrice: big( - currentChainRecord.chain.fees?.fee_tokens[0].average_gas_price ?? "0" - ).toFixed() as U, - }; - }, [currentChainName, currentChainRecord, fallbackGasPrice]); - - const chainBoundStates = useMemo(() => { - return { - indexerGraphClient: getIndexerGraphClient(currentChainName), - }; - }, [currentChainName]); - - const states = useMemo< - AppContextInterface - >( - () => ({ - chainGasPrice, - appContractAddress: appContractAddressMap(currentChainName), - appHumanAddress: appHumanAddressMap(currentChainName), - constants, - ...chainBoundStates, - }), - [ - chainGasPrice, - appContractAddressMap, - currentChainName, - appHumanAddressMap, - constants, - chainBoundStates, - ] - ); - - useEffect(() => { - if (currentChainName) { - const userKey = formatUserKey(currentChainName, DEFAULT_ADDRESS); - setCodeUserKey(userKey); - setContractUserKey(userKey); - setProjectUserKey(userKey); - } - }, [ - currentChainName, - setCodeUserKey, - setContractUserKey, - setProjectUserKey, - ]); - - useNetworkChange(); - - useAmplitude(); - - return isCodeUserKeyExist() && - isContractUserKeyExist() && - isProjectUserKeyExist() ? ( - {children} - ) : ( - - ); - } -); - -export const useApp = < - ContractAddress, - HumanAddress, - Constants extends AppConstants ->(): AppContextInterface => { +export const useCelatoneApp = (): AppContextInterface => { return useContext(AppContext); }; diff --git a/src/lib/app-provider/hooks/index.ts b/src/lib/app-provider/hooks/index.ts index 3d7901cdb..9f5478234 100644 --- a/src/lib/app-provider/hooks/index.ts +++ b/src/lib/app-provider/hooks/index.ts @@ -1,7 +1,5 @@ export * from "./useAddress"; export * from "./useAmplitude"; -export * from "./useCelatoneApp"; -export * from "./useChainId"; export * from "./useDummyWallet"; export * from "./useFabricateFee"; export * from "./useInternalNavigate"; @@ -10,6 +8,7 @@ export * from "./useMediaQuery"; export * from "./useNetworkChange"; export * from "./useRestrictedInput"; export * from "./useSelectChain"; -export * from "./useSimulateFee"; -export * from "./useTokensInfo"; -export * from "./useCurrentNetwork"; +export * from "./useBaseApiRoute"; +export * from "./useRPCEndpoint"; +export * from "./useConfig"; +export * from "./useCurrentChain"; diff --git a/src/lib/app-provider/hooks/useAddress.ts b/src/lib/app-provider/hooks/useAddress.ts index 0dfb996cd..3b1585b24 100644 --- a/src/lib/app-provider/hooks/useAddress.ts +++ b/src/lib/app-provider/hooks/useAddress.ts @@ -1,48 +1,46 @@ import { fromBech32 } from "@cosmjs/encoding"; -import type { ChainRecord } from "@cosmos-kit/core"; -import { useWallet } from "@cosmos-kit/react"; -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; +import { useCelatoneApp } from "../contexts"; import type { Option } from "lib/types"; +import { useCurrentChain } from "./useCurrentChain"; + export type AddressReturnType = | "user_address" | "contract_address" | "validator_address" | "invalid_address"; -const addressLengthMap: { - [key: string]: { [length: number]: AddressReturnType }; -} = { - osmosis: { - 43: "user_address", - 50: "validator_address", - 63: "contract_address", - }, - osmosistestnet5: { - 43: "user_address", - 50: "validator_address", - 63: "contract_address", - }, - terra2: { - 44: "user_address", - 51: "validator_address", - 64: "contract_address", - }, - terra2testnet: { - 44: "user_address", - 51: "validator_address", - 64: "contract_address", - }, +export const useGetAddressTypeByLength = () => { + const { + chainConfig: { exampleAddresses }, + } = useCelatoneApp(); + const addressLengthMap = useMemo( + () => + Object.entries(exampleAddresses).reduce<{ + [key: number]: AddressReturnType; + }>( + (acc, curr) => ({ + ...acc, + [curr[1].length]: `${curr[0]}_address` as AddressReturnType, + }), + {} + ), + [exampleAddresses] + ); + return useCallback( + (address: Option): AddressReturnType => + address + ? addressLengthMap[address.length] ?? "invalid_address" + : "invalid_address", + [addressLengthMap] + ); }; -export const getAddressTypeByLength = ( - chainName: string, - address: Option -): AddressReturnType => - address - ? addressLengthMap[chainName]?.[address.length] ?? "invalid_address" - : "invalid_address"; +export type GetAddressTypeByLengthFn = ReturnType< + typeof useGetAddressTypeByLength +>; const getPrefix = (basePrefix: string, addressType: AddressReturnType) => { if (addressType === "validator_address") return `${basePrefix}valoper`; @@ -50,18 +48,20 @@ const getPrefix = (basePrefix: string, addressType: AddressReturnType) => { }; const validateAddress = ( - currentChainRecord: ChainRecord | undefined, + bech32Prefix: string, address: string, - addressType: AddressReturnType + addressType: AddressReturnType, + getAddressTypeByLength: GetAddressTypeByLengthFn ) => { - if (!currentChainRecord) return "Invalid network"; + if (!bech32Prefix) + return "Can not retrieve bech32 prefix of the current network."; - const prefix = getPrefix(currentChainRecord.chain.bech32_prefix, addressType); + const prefix = getPrefix(bech32Prefix, addressType); if (!address.startsWith(prefix)) return `Invalid prefix (expected "${prefix}")`; - if (getAddressTypeByLength(currentChainRecord.name, address) !== addressType) + if (getAddressTypeByLength(address) !== addressType) return "Invalid address length"; try { @@ -73,41 +73,67 @@ const validateAddress = ( }; export const useGetAddressType = () => { - const { currentChainName, currentChainRecord } = useWallet(); + const { + chain: { bech32_prefix: bech32Prefix }, + } = useCurrentChain(); + const getAddressTypeByLength = useGetAddressTypeByLength(); return useCallback( (address: Option): AddressReturnType => { - const addressType = getAddressTypeByLength(currentChainName, address); + const addressType = getAddressTypeByLength(address); if ( !address || addressType === "invalid_address" || - validateAddress(currentChainRecord, address, addressType) + validateAddress( + bech32Prefix, + address, + addressType, + getAddressTypeByLength + ) ) return "invalid_address"; return addressType; }, - [currentChainName, currentChainRecord] + [bech32Prefix, getAddressTypeByLength] ); }; // TODO: refactor export const useValidateAddress = () => { - const { currentChainRecord } = useWallet(); + const { + chain: { bech32_prefix: bech32Prefix }, + } = useCurrentChain(); + const getAddressTypeByLength = useGetAddressTypeByLength(); return { validateContractAddress: useCallback( (address: string) => - validateAddress(currentChainRecord, address, "contract_address"), - [currentChainRecord] + validateAddress( + bech32Prefix, + address, + "contract_address", + getAddressTypeByLength + ), + [bech32Prefix, getAddressTypeByLength] ), validateUserAddress: useCallback( (address: string) => - validateAddress(currentChainRecord, address, "user_address"), - [currentChainRecord] + validateAddress( + bech32Prefix, + address, + "user_address", + getAddressTypeByLength + ), + [bech32Prefix, getAddressTypeByLength] ), validateValidatorAddress: useCallback( (address: string) => - validateAddress(currentChainRecord, address, "validator_address"), - [currentChainRecord] + validateAddress( + bech32Prefix, + address, + "validator_address", + getAddressTypeByLength + ), + [bech32Prefix, getAddressTypeByLength] ), }; }; diff --git a/src/lib/app-provider/hooks/useAmplitude.ts b/src/lib/app-provider/hooks/useAmplitude.ts index 1893730f5..95cc5d226 100644 --- a/src/lib/app-provider/hooks/useAmplitude.ts +++ b/src/lib/app-provider/hooks/useAmplitude.ts @@ -1,12 +1,16 @@ import { init, setDeviceId, setUserId } from "@amplitude/analytics-browser"; -import { useWallet } from "@cosmos-kit/react"; +import { useChain } from "@cosmos-kit/react"; import { createHash } from "crypto"; import { useEffect } from "react"; import * as uuid from "uuid"; -export const useAmplitude = () => { - const { address, currentChainName } = useWallet(); +import type { Option } from "lib/types"; +export const useAmplitude = (chainName: Option) => { + /** + * @remarks Revisit default chain name + */ + const { address } = useChain(chainName ?? "osmosis"); if (typeof window !== "undefined") { init(process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY ?? "", undefined, { trackingOptions: { @@ -31,13 +35,13 @@ export const useAmplitude = () => { useEffect(() => { const timeoutId = setTimeout(() => { - if (currentChainName) { + if (chainName) { const userId = address ? createHash("sha256").update(address).digest("hex") : undefined; - setUserId(`${currentChainName}/${userId}`); + setUserId(`${chainName}/${userId}`); } }, 300); return () => clearTimeout(timeoutId); - }, [address, currentChainName]); + }, [address, chainName]); }; diff --git a/src/lib/app-provider/hooks/useAttachFunds.ts b/src/lib/app-provider/hooks/useAttachFunds.ts new file mode 100644 index 000000000..248c68a2f --- /dev/null +++ b/src/lib/app-provider/hooks/useAttachFunds.ts @@ -0,0 +1,40 @@ +import type { Coin } from "@cosmjs/stargate"; +import { useCallback } from "react"; + +import { AttachFundsType } from "lib/components/fund/types"; +import { useAssetInfos } from "lib/services/assetService"; +import { fabricateFunds, sortDenoms } from "lib/utils"; + +export const useAttachFunds = () => { + const { assetInfos } = useAssetInfos(); + + return useCallback( + ( + attachFundsOption: AttachFundsType, + assetsJsonStr: string, + assetsSelect: Coin[] + ) => { + const assetsSelectWithPrecision = assetsSelect.map((coin) => { + return { + ...coin, + precision: assetInfos?.[coin.denom]?.precision, + }; + }); + + switch (attachFundsOption) { + case AttachFundsType.ATTACH_FUNDS_SELECT: + return fabricateFunds(assetsSelectWithPrecision); + case AttachFundsType.ATTACH_FUNDS_JSON: + try { + return sortDenoms(JSON.parse(assetsJsonStr)); + } catch { + return []; + } + default: + return []; + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(assetInfos)] + ); +}; diff --git a/src/lib/app-provider/hooks/useBaseApiRoute.ts b/src/lib/app-provider/hooks/useBaseApiRoute.ts new file mode 100644 index 000000000..95d877f96 --- /dev/null +++ b/src/lib/app-provider/hooks/useBaseApiRoute.ts @@ -0,0 +1,55 @@ +import { useCelatoneApp } from "../contexts"; +import { CELATONE_API_OVERRIDE } from "env"; + +export const useBaseApiRoute = ( + type: + | "txs" + | "balances" + | "assets" + | "projects" + | "contracts" + | "codes" + | "accounts" + | "rest" + | "native_tokens" + | "cosmwasm" +): string => { + const { + chainConfig: { chain, api: configApi }, + currentChainId, + } = useCelatoneApp(); + + if (!chain || !currentChainId || !configApi) + throw new Error( + "Error retrieving chain, api, or currentChainId from chain config." + ); + + const api = CELATONE_API_OVERRIDE || configApi; + + switch (type) { + case "txs": + return `${api}/txs/${chain}/${currentChainId}`; + case "balances": + return `${api}/balances/${chain}/${currentChainId}`; + case "assets": + return `${api}/assets/${chain}/${currentChainId}`; + case "projects": + return `${api}/projects/${chain}/${currentChainId}`; + case "contracts": + return `${api}/contracts/${chain}/${currentChainId}`; + case "codes": + return `${api}/codes/${chain}/${currentChainId}`; + case "accounts": + return `${api}/accounts/${chain}/${currentChainId}`; + case "rest": + return `${api}/rest/${chain}/${currentChainId}`; + case "native_tokens": + return `${api}/native-assets/${chain}/${currentChainId}`; + case "cosmwasm": + return `${api}/cosmwasm/${chain}/${currentChainId}`; + default: + throw new Error( + "Error retrieving chain, api, or currentChainId from chain config." + ); + } +}; diff --git a/src/lib/app-provider/hooks/useCelatoneApp.ts b/src/lib/app-provider/hooks/useCelatoneApp.ts deleted file mode 100644 index 41ca60850..000000000 --- a/src/lib/app-provider/hooks/useCelatoneApp.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useApp } from "../contexts"; -import type { - CelatoneConstants, - CelatoneContractAddress, - CelatoneHumanAddress, -} from "types"; - -export const useCelatoneApp = () => { - return useApp< - CelatoneContractAddress, - CelatoneHumanAddress, - CelatoneConstants - >(); -}; diff --git a/src/lib/app-provider/hooks/useChainId.ts b/src/lib/app-provider/hooks/useChainId.ts deleted file mode 100644 index 31f9b9eb4..000000000 --- a/src/lib/app-provider/hooks/useChainId.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useWallet } from "@cosmos-kit/react"; - -export const useChainId = () => { - const { currentChainRecord } = useWallet(); - const chainId = currentChainRecord?.chain.chain_id; - if (!chainId) throw new Error("Chain ID not found"); - return chainId; -}; diff --git a/src/lib/app-provider/hooks/useConfig.ts b/src/lib/app-provider/hooks/useConfig.ts new file mode 100644 index 000000000..c9bae5d12 --- /dev/null +++ b/src/lib/app-provider/hooks/useConfig.ts @@ -0,0 +1,81 @@ +import { useCelatoneApp } from "../contexts"; +import type { ChainConfig } from "config/types"; + +import { useInternalNavigate } from "./useInternalNavigate"; + +type Features = ChainConfig["features"]; + +type FeatureVariant = Features[keyof Features]; + +interface BaseConfigArgs { + feature: Feature; + shouldRedirect: boolean; +} + +const useBaseConfig = ({ + feature, + shouldRedirect, +}: BaseConfigArgs) => { + const navigate = useInternalNavigate(); + + if (!feature.enabled && shouldRedirect) + navigate({ pathname: "/", replace: true }); + + return feature; +}; + +export const useWasmConfig = ({ + shouldRedirect, +}: { + shouldRedirect: boolean; +}) => { + const { + chainConfig: { + features: { wasm }, + }, + } = useCelatoneApp(); + + return useBaseConfig({ feature: wasm, shouldRedirect }); +}; + +export const useFaucetConfig = ({ + shouldRedirect, +}: { + shouldRedirect: boolean; +}) => { + const { + chainConfig: { + features: { faucet }, + }, + } = useCelatoneApp(); + + return useBaseConfig({ feature: faucet, shouldRedirect }); +}; + +export const usePoolConfig = ({ + shouldRedirect, +}: { + shouldRedirect: boolean; +}) => { + const { + chainConfig: { + features: { pool }, + }, + } = useCelatoneApp(); + + return useBaseConfig({ feature: pool, shouldRedirect }); +}; + +export const usePublicProjectConfig = ({ + shouldRedirect, +}: { + shouldRedirect: boolean; +}) => { + const { + chainConfig: { + features: { publicProject }, + }, + } = useCelatoneApp(); + + return useBaseConfig({ feature: publicProject, shouldRedirect }); +}; diff --git a/src/lib/app-provider/hooks/useCurrentChain.ts b/src/lib/app-provider/hooks/useCurrentChain.ts new file mode 100644 index 000000000..c961c3fc2 --- /dev/null +++ b/src/lib/app-provider/hooks/useCurrentChain.ts @@ -0,0 +1,10 @@ +import { useChain } from "@cosmos-kit/react"; + +import { useCelatoneApp } from "../contexts"; + +export const useCurrentChain = () => { + const { + chainConfig: { registryChainName }, + } = useCelatoneApp(); + return useChain(registryChainName); +}; diff --git a/src/lib/app-provider/hooks/useCurrentNetwork.ts b/src/lib/app-provider/hooks/useCurrentNetwork.ts deleted file mode 100644 index d7675b535..000000000 --- a/src/lib/app-provider/hooks/useCurrentNetwork.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useWallet } from "@cosmos-kit/react"; -import { useRouter } from "next/router"; - -import { getNetworkByChainName } from "lib/data"; -import type { Network } from "lib/data"; - -export const useCurrentNetwork = () => { - const { currentChainName } = useWallet(); - const router = useRouter(); - - // Revisit: this is a hack to fix the issue of the walletManager being - try { - const network = getNetworkByChainName(currentChainName); - return { - network, - isMainnet: network === "mainnet", - isTestnet: network === "testnet", - isLocalnet: network === "localnet", - }; - } catch (e) { - window.localStorage.removeItem("walletManager"); - - // eslint-disable-next-line no-console - console.log("remove walletManager"); - router.reload(); - - // This is mock value, it will be replaced by the real value after the page is reloaded - return { - network: "mainnet" as Network, - isMainnet: true, - isTestnet: false, - isLocalnet: false, - }; - } -}; diff --git a/src/lib/app-provider/hooks/useDummyWallet.ts b/src/lib/app-provider/hooks/useDummyWallet.ts index 12553ecae..8d6c824f9 100644 --- a/src/lib/app-provider/hooks/useDummyWallet.ts +++ b/src/lib/app-provider/hooks/useDummyWallet.ts @@ -1,12 +1,13 @@ import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { useWallet } from "@cosmos-kit/react"; import { useEffect, useState } from "react"; import { DUMMY_MNEMONIC } from "env"; import type { HumanAddr } from "lib/types"; +import { useCurrentChain } from "./useCurrentChain"; + export const useDummyWallet = () => { - const { currentChainRecord } = useWallet(); + const { chain } = useCurrentChain(); const [dummyWallet, setDummyWallet] = useState(); const [dummyAddress, setDummyAddress] = useState(); useEffect(() => { @@ -15,7 +16,7 @@ export const useDummyWallet = () => { const wallet = await DirectSecp256k1HdWallet.fromMnemonic( DUMMY_MNEMONIC, { - prefix: currentChainRecord?.chain.bech32_prefix, + prefix: chain.bech32_prefix, } ); @@ -25,7 +26,7 @@ export const useDummyWallet = () => { setDummyAddress(address as HumanAddr); } })(); - }, [currentChainRecord?.chain.bech32_prefix]); + }, [chain.bech32_prefix]); return { dummyWallet, dummyAddress }; }; diff --git a/src/lib/app-provider/hooks/useFabricateFee.ts b/src/lib/app-provider/hooks/useFabricateFee.ts index ee7999547..f31a1af1c 100644 --- a/src/lib/app-provider/hooks/useFabricateFee.ts +++ b/src/lib/app-provider/hooks/useFabricateFee.ts @@ -2,29 +2,33 @@ import type { StdFee } from "@cosmjs/stargate"; import big from "big.js"; import { useCallback } from "react"; +import { useCelatoneApp } from "../contexts/app"; import type { Gas } from "lib/types"; -import { useCelatoneApp } from "./useCelatoneApp"; - export const useFabricateFee = () => { - const { constants, chainGasPrice } = useCelatoneApp(); + const { + chainConfig: { + gas: { gasPrice, gasAdjustment, maxGasLimit }, + }, + } = useCelatoneApp(); return useCallback( (estimatedGas: number): StdFee => { - const adjustedGas = big(estimatedGas) - .mul(constants.gasAdjustment) - .toFixed(0); + const adjustedGas = Math.min( + Number(big(estimatedGas).mul(gasAdjustment).toFixed(0)), + maxGasLimit + ); return { amount: [ { - denom: chainGasPrice.denom, - amount: big(adjustedGas).mul(chainGasPrice.gasPrice).toFixed(0), + denom: gasPrice.denom, + amount: big(adjustedGas).mul(gasPrice.tokenPerGas).toFixed(0), }, ], - gas: adjustedGas as Gas, + gas: adjustedGas.toString() as Gas, }; }, - [chainGasPrice, constants.gasAdjustment] + [gasAdjustment, gasPrice.denom, gasPrice.tokenPerGas, maxGasLimit] ); }; diff --git a/src/lib/app-provider/hooks/useInternalNavigate.ts b/src/lib/app-provider/hooks/useInternalNavigate.ts index d130d4978..1487fcce8 100644 --- a/src/lib/app-provider/hooks/useInternalNavigate.ts +++ b/src/lib/app-provider/hooks/useInternalNavigate.ts @@ -3,6 +3,9 @@ import type { Router } from "next/router"; import type { ParsedUrlQueryInput } from "node:querystring"; import { useCallback } from "react"; +import { DEFAULT_SUPPORTED_CHAIN_ID, SUPPORTED_CHAIN_IDS } from "env"; +import { getFirstQueryParam } from "lib/utils"; + export interface NavigationArgs { pathname: string; query?: ParsedUrlQueryInput; @@ -25,7 +28,11 @@ export const useInternalNavigate = () => { { pathname: `/[network]${pathname}`, query: { - network: router.query.network === "testnet" ? "testnet" : "mainnet", + network: SUPPORTED_CHAIN_IDS.includes( + getFirstQueryParam(router.query.network) + ) + ? router.query.network + : DEFAULT_SUPPORTED_CHAIN_ID, ...query, }, }, diff --git a/src/lib/app-provider/hooks/useLCDEndpoint.ts b/src/lib/app-provider/hooks/useLCDEndpoint.ts index 4eb9f1442..628aebac1 100644 --- a/src/lib/app-provider/hooks/useLCDEndpoint.ts +++ b/src/lib/app-provider/hooks/useLCDEndpoint.ts @@ -1,12 +1,13 @@ -import { useWallet } from "@cosmos-kit/react"; +import { useCelatoneApp } from "../contexts"; -import { FALLBACK_LCD_ENDPOINT } from "env"; +import { useCurrentChain } from "./useCurrentChain"; export const useLCDEndpoint = () => { - const { currentChainRecord, currentChainName } = useWallet(); + const { chainWallet } = useCurrentChain(); + const { chainConfig } = useCelatoneApp(); + const restRecord = chainWallet?.chainRecord.preferredEndpoints?.rest?.[0]; + const endpoint = + typeof restRecord === "string" ? restRecord : restRecord?.url; - return ( - currentChainRecord?.preferredEndpoints?.rest?.[0] ?? - FALLBACK_LCD_ENDPOINT[currentChainName] - ); + return endpoint ?? chainConfig.lcd; }; diff --git a/src/lib/app-provider/hooks/useMediaQuery.ts b/src/lib/app-provider/hooks/useMediaQuery.ts index 82d5bda47..da5491531 100644 --- a/src/lib/app-provider/hooks/useMediaQuery.ts +++ b/src/lib/app-provider/hooks/useMediaQuery.ts @@ -1,3 +1,3 @@ import { useMediaQuery } from "react-responsive"; -export const useMobile = () => useMediaQuery({ query: "(max-width: 540px)" }); +export const useMobile = () => useMediaQuery({ query: "(max-width: 767px)" }); diff --git a/src/lib/app-provider/hooks/useNetworkChange.ts b/src/lib/app-provider/hooks/useNetworkChange.ts index 30f233efd..7e8facfb7 100644 --- a/src/lib/app-provider/hooks/useNetworkChange.ts +++ b/src/lib/app-provider/hooks/useNetworkChange.ts @@ -1,35 +1,50 @@ -import { useWallet } from "@cosmos-kit/react"; import { useRouter } from "next/router"; import { useEffect, useRef } from "react"; -import { getChainNameByNetwork } from "lib/data"; -import type { Network } from "lib/data"; +import { DEFAULT_SUPPORTED_CHAIN_ID, SUPPORTED_CHAIN_IDS } from "env"; import { getFirstQueryParam } from "lib/utils"; -export const useNetworkChange = () => { +import { useInternalNavigate } from "./useInternalNavigate"; + +export const useNetworkChange = ( + handleOnChainIdChange: (newChainId: string) => void +) => { const router = useRouter(); - const { currentChainName, setCurrentChain } = useWallet(); const networkRef = useRef(); + const navigate = useInternalNavigate(); useEffect(() => { - if (router.isReady) { - let networkRoute = getFirstQueryParam( - router.query.network, - "mainnet" - ) as Network; - - if ( - networkRoute !== "mainnet" && - networkRoute !== "testnet" && - networkRoute !== "localnet" - ) - networkRoute = "mainnet"; + const networkRoute = router.query.network + ? getFirstQueryParam(router.query.network, DEFAULT_SUPPORTED_CHAIN_ID) + : router.asPath.split("/")[1]; - if (networkRoute !== networkRef.current) { + if (router.isReady || router.pathname === "/404") { + // Redirect to default chain if there is no network query provided + if (!router.query.network) { + navigate({ + pathname: router.pathname, + query: { ...router.query }, + replace: true, + }); + } else if ( + router.pathname === "/[network]" && + !SUPPORTED_CHAIN_IDS.includes(networkRoute) + ) { + navigate({ + pathname: "/", + replace: true, + }); + } else if (networkRoute !== networkRef.current) { networkRef.current = networkRoute; - const chainName = getChainNameByNetwork(networkRoute); - if (currentChainName !== chainName) setCurrentChain(chainName); + handleOnChainIdChange(networkRoute); } } - }, [router, currentChainName, setCurrentChain]); + }, [ + handleOnChainIdChange, + navigate, + router.asPath, + router.isReady, + router.pathname, + router.query, + ]); }; diff --git a/src/lib/app-provider/hooks/useRPCEndpoint.ts b/src/lib/app-provider/hooks/useRPCEndpoint.ts new file mode 100644 index 000000000..7017bef18 --- /dev/null +++ b/src/lib/app-provider/hooks/useRPCEndpoint.ts @@ -0,0 +1,12 @@ +import { useCelatoneApp } from "../contexts"; + +import { useCurrentChain } from "./useCurrentChain"; + +export const useRPCEndpoint = () => { + const { chainWallet } = useCurrentChain(); + const { chainConfig } = useCelatoneApp(); + const rpcRecord = chainWallet?.chainRecord.preferredEndpoints?.rpc?.[0]; + const endpoint = typeof rpcRecord === "string" ? rpcRecord : rpcRecord?.url; + + return endpoint ?? chainConfig.rpc; +}; diff --git a/src/lib/app-provider/hooks/useSelectChain.ts b/src/lib/app-provider/hooks/useSelectChain.ts index 446566703..ab5282da7 100644 --- a/src/lib/app-provider/hooks/useSelectChain.ts +++ b/src/lib/app-provider/hooks/useSelectChain.ts @@ -1,27 +1,24 @@ -import { useWallet } from "@cosmos-kit/react"; import { useRouter } from "next/router"; import { useCallback } from "react"; -import { useInternalNavigate } from "lib/app-provider/hooks/useInternalNavigate"; -import { getNetworkByChainName } from "lib/data"; +import { useInternalNavigate } from "./useInternalNavigate"; export const useSelectChain = () => { const router = useRouter(); - const { currentChainName, setCurrentChain } = useWallet(); - const navigate = useInternalNavigate(); + const navigator = useInternalNavigate(); return useCallback( - (chainName: string) => { - if (chainName === currentChainName) return; - setCurrentChain(chainName); - navigate({ + (chainId: string) => { + if (router.query.network === chainId) return; + + navigator({ pathname: router.pathname.replace("/[network]", ""), query: { ...router.query, - network: getNetworkByChainName(chainName), + network: chainId, }, }); }, - [currentChainName, setCurrentChain, navigate, router] + [navigator, router.pathname, router.query] ); }; diff --git a/src/lib/app-provider/hooks/useSimulateFee.ts b/src/lib/app-provider/hooks/useSimulateFee.ts deleted file mode 100644 index f8cb3e918..000000000 --- a/src/lib/app-provider/hooks/useSimulateFee.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useWallet } from "@cosmos-kit/react"; -import { useCallback, useState } from "react"; - -import type { Gas } from "lib/types"; -import type { ComposedMsg } from "lib/types/tx"; - -// TODO: remove this hook after migrating to useQuery version -export const useSimulateFee = () => { - const { address, getCosmWasmClient } = useWallet(); - - const [loading, setLoading] = useState(false); - - const simulate = useCallback( - async (messages: ComposedMsg[], memo?: string) => { - setLoading(true); - const client = await getCosmWasmClient(); - if (!client || !address) { - setLoading(false); - return undefined; - } - try { - const fee = (await client.simulate(address, messages, memo)) as Gas; - setLoading(false); - return fee; - } catch (e) { - setLoading(false); - throw e; - } - }, - [address, getCosmWasmClient, setLoading] - ); - - return { simulate, loading }; -}; diff --git a/src/lib/app-provider/hooks/useTokensInfo.ts b/src/lib/app-provider/hooks/useTokensInfo.ts deleted file mode 100644 index fb8638f46..000000000 --- a/src/lib/app-provider/hooks/useTokensInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useWallet } from "@cosmos-kit/react"; -import { useMemo } from "react"; - -export const useNativeTokensInfo = () => { - const { currentChainRecord } = useWallet(); - - return useMemo( - () => - currentChainRecord?.assetList?.assets?.filter( - (asset) => !asset.base.includes("cw20") - ) ?? [], - [currentChainRecord] - ); -}; diff --git a/src/lib/app-provider/queries/simulateFee.ts b/src/lib/app-provider/queries/simulateFee.ts index a7980b551..aebf36772 100644 --- a/src/lib/app-provider/queries/simulateFee.ts +++ b/src/lib/app-provider/queries/simulateFee.ts @@ -1,9 +1,18 @@ +import type { Coin } from "@cosmjs/amino"; import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; -import { useWallet } from "@cosmos-kit/react"; import { useQuery } from "@tanstack/react-query"; +import { gzip } from "node-gzip"; -import { useDummyWallet } from "../hooks"; -import type { ComposedMsg, Gas } from "lib/types"; +import { useCurrentChain, useDummyWallet, useRPCEndpoint } from "../hooks"; +import type { + AccessType, + Addr, + ComposedMsg, + Gas, + HumanAddr, + Option, +} from "lib/types"; +import { composeStoreCodeMsg, composeStoreCodeProposalMsg } from "lib/utils"; interface SimulateQueryParams { enabled: boolean; @@ -20,25 +29,24 @@ export const useSimulateFeeQuery = ({ onSuccess, onError, }: SimulateQueryParams) => { - const { address, getCosmWasmClient, currentChainName, currentChainRecord } = - useWallet(); + const { address, getSigningCosmWasmClient, chain } = useCurrentChain(); const { dummyWallet, dummyAddress } = useDummyWallet(); - + const rpcEndpoint = useRPCEndpoint(); const userAddress = isDummyUser ? dummyAddress : address || dummyAddress; const simulateFn = async (msgs: ComposedMsg[]) => { // TODO: revisit this logic - if (!currentChainRecord?.preferredEndpoints?.rpc?.[0] || !userAddress) { - throw new Error("No RPC endpoint or user address"); + if (!userAddress) { + throw new Error("No user address"); } const client = dummyWallet && (isDummyUser || !address) ? await SigningCosmWasmClient.connectWithSigner( - currentChainRecord.preferredEndpoints.rpc[0], + rpcEndpoint, dummyWallet ) - : await getCosmWasmClient(); + : await getSigningCosmWasmClient(); if (!client) { throw new Error("Fail to get SigningCosmWasmClient"); @@ -48,11 +56,160 @@ export const useSimulateFeeQuery = ({ }; return useQuery({ - queryKey: ["simulate", currentChainName, userAddress, messages], + queryKey: [ + "simulate", + chain.chain_name, + userAddress, + messages, + rpcEndpoint, + ], queryFn: async ({ queryKey }) => simulateFn(queryKey[3] as ComposedMsg[]), enabled, - keepPreviousData: true, - retry: false, + retry: 2, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + onSuccess, + onError, + }); +}; + +interface SimulateQueryParamsForStoreCode { + enabled: boolean; + wasmFile: Option; + permission: AccessType; + addresses: Addr[]; + onSuccess?: (gas: Gas | undefined) => void; + onError?: (err: Error) => void; +} + +export const useSimulateFeeForStoreCode = ({ + enabled, + wasmFile, + permission, + addresses, + onSuccess, + onError, +}: SimulateQueryParamsForStoreCode) => { + const { address, getSigningCosmWasmClient, chain } = useCurrentChain(); + const simulateFn = async () => { + if (!address) throw new Error("Please check your wallet connection."); + if (!wasmFile) throw new Error("Fail to get Wasm file"); + + const client = await getSigningCosmWasmClient(); + if (!client) throw new Error("Fail to get client"); + + const submitStoreCodeMsg = async () => { + return composeStoreCodeMsg({ + sender: address as HumanAddr, + wasmByteCode: await gzip(new Uint8Array(await wasmFile.arrayBuffer())), + permission, + addresses, + }); + }; + const craftMsg = await submitStoreCodeMsg(); + return (await client.simulate(address, [craftMsg], undefined)) as Gas; + }; + return useQuery({ + queryKey: [ + "simulate_fee_store_code", + chain.chain_name, + wasmFile, + permission, + addresses, + ], + queryFn: async () => simulateFn(), + enabled, + retry: 2, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + onSuccess, + onError, + }); +}; + +interface SimulateQueryParamsForProposalStoreCode { + enabled: boolean; + title: string; + description: string; + runAs: Addr; + initialDeposit: Coin; + unpinCode: boolean; + builder: string; + source: string; + codeHash: string; + wasmFile: Option; + permission: AccessType; + addresses: Addr[]; + precision: Option; + onSuccess?: (gas: Gas | undefined) => void; + onError?: (err: Error) => void; +} + +export const useSimulateFeeForProposalStoreCode = ({ + enabled, + title, + description, + runAs, + initialDeposit, + unpinCode, + builder, + source, + codeHash, + wasmFile, + permission, + addresses, + precision, + onSuccess, + onError, +}: SimulateQueryParamsForProposalStoreCode) => { + const { address, getSigningCosmWasmClient, chain } = useCurrentChain(); + const simulateFn = async () => { + if (!address) throw new Error("Please check your wallet connection."); + if (!wasmFile) throw new Error("Fail to get Wasm file"); + + const client = await getSigningCosmWasmClient(); + if (!client) throw new Error("Fail to get client"); + + const submitStoreCodeProposalMsg = async () => { + return composeStoreCodeProposalMsg({ + proposer: address as HumanAddr, + title, + description, + runAs: runAs as Addr, + wasmByteCode: await gzip(new Uint8Array(await wasmFile.arrayBuffer())), + permission, + addresses, + unpinCode, + source, + builder, + codeHash: Uint8Array.from(Buffer.from(codeHash, "hex")), + initialDeposit, + precision, + }); + }; + + const craftMsg = await submitStoreCodeProposalMsg(); + return (await client.simulate(address, [craftMsg], undefined)) as Gas; + }; + + return useQuery({ + queryKey: [ + "simulate_fee_store_code_proposal", + chain.chain_name, + runAs, + initialDeposit, + unpinCode, + builder, + source, + codeHash, + wasmFile, + permission, + addresses, + enabled, + ], + queryFn: async () => simulateFn(), + enabled, + retry: 2, refetchOnReconnect: false, refetchOnWindowFocus: false, onSuccess, diff --git a/src/lib/app-provider/query-client/index.ts b/src/lib/app-provider/query-client/index.ts deleted file mode 100644 index 74464ceff..000000000 --- a/src/lib/app-provider/query-client/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GraphQLClient } from "graphql-request"; - -export const GRAPH_URL: Record = { - /** - * Revisit graphql for terra2 mainnet and osmosis mainnet - */ - osmosis: "https://osmosis-mainnet-graphql.alleslabs.dev/v1/graphql", - osmosistestnet5: "https://osmo-test-5-graphql.alleslabs.dev/v1/graphql", - terra2testnet: "https://terra-testnet-graphql.alleslabs.dev/v1/graphql", -}; - -export const getIndexerGraphClient = (currentChainName: string) => - new GraphQLClient(GRAPH_URL[currentChainName]); diff --git a/src/lib/app-provider/tx/clearAdmin.ts b/src/lib/app-provider/tx/clearAdmin.ts index 22972e9f1..13c360257 100644 --- a/src/lib/app-provider/tx/clearAdmin.ts +++ b/src/lib/app-provider/tx/clearAdmin.ts @@ -1,10 +1,8 @@ -import { useWallet } from "@cosmos-kit/react"; import { useQueryClient } from "@tanstack/react-query"; import { useCallback } from "react"; -import { useFabricateFee } from "../hooks"; +import { useCurrentChain, useFabricateFee, useWasmConfig } from "../hooks"; import { clearAdminTx } from "lib/app-fns/tx/clearAdmin"; -import { CLEAR_ADMIN_GAS } from "lib/data"; import type { ContractAddr, HumanAddr } from "lib/types"; export interface ClearAdminStreamParams { @@ -12,17 +10,24 @@ export interface ClearAdminStreamParams { } export const useClearAdminTx = (contractAddress: ContractAddr) => { - const { address, getCosmWasmClient } = useWallet(); + const { address, getSigningCosmWasmClient } = useCurrentChain(); const queryClient = useQueryClient(); const fabricateFee = useFabricateFee(); - const clearAdminFee = fabricateFee(CLEAR_ADMIN_GAS); + const wasm = useWasmConfig({ shouldRedirect: false }); return useCallback( async ({ onTxSucceed }: ClearAdminStreamParams) => { - const client = await getCosmWasmClient(); + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); + if (!wasm.enabled) + throw new Error( + "Wasm config isn't loaded or Wasm feature is disabled." + ); + + const clearAdminFee = fabricateFee(wasm.clearAdminGas); + return clearAdminTx({ address: address as HumanAddr, contractAddress, @@ -41,6 +46,13 @@ export const useClearAdminTx = (contractAddress: ContractAddr) => { }, }); }, - [address, clearAdminFee, queryClient, contractAddress, getCosmWasmClient] + [ + getSigningCosmWasmClient, + address, + wasm, + fabricateFee, + contractAddress, + queryClient, + ] ); }; diff --git a/src/lib/app-provider/tx/execute.ts b/src/lib/app-provider/tx/execute.ts index 0245311db..670efff5e 100644 --- a/src/lib/app-provider/tx/execute.ts +++ b/src/lib/app-provider/tx/execute.ts @@ -1,7 +1,7 @@ import type { Coin, StdFee } from "@cosmjs/stargate"; -import { useWallet } from "@cosmos-kit/react"; import { useCallback } from "react"; +import { useCurrentChain } from "../hooks"; import { executeContractTx } from "lib/app-fns/tx/execute"; import { useUserKey } from "lib/hooks/useUserKey"; import type { Activity } from "lib/stores/contract"; @@ -17,7 +17,7 @@ export interface ExecuteStreamParams { } export const useExecuteContractTx = () => { - const { address, getCosmWasmClient } = useWallet(); + const { address, getSigningCosmWasmClient } = useCurrentChain(); const userKey = useUserKey(); return useCallback( @@ -29,7 +29,7 @@ export const useExecuteContractTx = () => { msg, funds, }: ExecuteStreamParams) => { - const client = await getCosmWasmClient(); + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); if (!estimatedFee) return null; @@ -46,6 +46,6 @@ export const useExecuteContractTx = () => { onTxFailed, }); }, - [address, userKey, getCosmWasmClient] + [address, userKey, getSigningCosmWasmClient] ); }; diff --git a/src/lib/app-provider/tx/instantiate.ts b/src/lib/app-provider/tx/instantiate.ts index 2b894cd4e..ef3935eef 100644 --- a/src/lib/app-provider/tx/instantiate.ts +++ b/src/lib/app-provider/tx/instantiate.ts @@ -1,34 +1,36 @@ import type { InstantiateResult } from "@cosmjs/cosmwasm-stargate"; import type { Coin, StdFee } from "@cosmjs/stargate"; -import { useWallet } from "@cosmos-kit/react"; import { useCallback } from "react"; +import { useCurrentChain } from "../hooks"; import { instantiateContractTx } from "lib/app-fns/tx/instantiate"; export interface InstantiateStreamParams { - onTxSucceed?: (txResult: InstantiateResult, contractLabel: string) => void; estimatedFee: StdFee | undefined; codeId: number; initMsg: object; label: string; admin: string; funds: Coin[]; + onTxSucceed?: (txResult: InstantiateResult, contractLabel: string) => void; + onTxFailed?: () => void; } export const useInstantiateTx = () => { - const { address, getCosmWasmClient } = useWallet(); + const { address, getSigningCosmWasmClient } = useCurrentChain(); return useCallback( async ({ - onTxSucceed, estimatedFee, codeId, initMsg, label, admin, funds, + onTxSucceed, + onTxFailed, }: InstantiateStreamParams) => { - const client = await getCosmWasmClient(); + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); if (!estimatedFee) return null; @@ -43,8 +45,9 @@ export const useInstantiateTx = () => { funds, client, onTxSucceed, + onTxFailed, }); }, - [address, getCosmWasmClient] + [address, getSigningCosmWasmClient] ); }; diff --git a/src/lib/app-provider/tx/migrate.ts b/src/lib/app-provider/tx/migrate.ts index a23aa9cba..804f83f74 100644 --- a/src/lib/app-provider/tx/migrate.ts +++ b/src/lib/app-provider/tx/migrate.ts @@ -1,7 +1,7 @@ import type { StdFee } from "@cosmjs/stargate"; -import { useWallet } from "@cosmos-kit/react"; import { useCallback } from "react"; +import { useCurrentChain } from "../hooks"; import { migrateContractTx } from "lib/app-fns/tx/migrate"; import type { ContractAddr, HumanAddr, Option } from "lib/types"; @@ -15,8 +15,7 @@ export interface MigrateStreamParams { } export const useMigrateTx = () => { - const { address, getCosmWasmClient } = useWallet(); - + const { address, getSigningCosmWasmClient } = useCurrentChain(); return useCallback( async ({ contractAddress, @@ -26,7 +25,7 @@ export const useMigrateTx = () => { onTxSucceed, onTxFailed, }: MigrateStreamParams) => { - const client = await getCosmWasmClient(); + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); if (!estimatedFee) return null; @@ -42,6 +41,6 @@ export const useMigrateTx = () => { onTxFailed, }); }, - [address, getCosmWasmClient] + [address, getSigningCosmWasmClient] ); }; diff --git a/src/lib/app-provider/tx/resend.ts b/src/lib/app-provider/tx/resend.ts index 87854ed36..faa19911d 100644 --- a/src/lib/app-provider/tx/resend.ts +++ b/src/lib/app-provider/tx/resend.ts @@ -1,8 +1,8 @@ import type { EncodeObject } from "@cosmjs/proto-signing"; import type { StdFee } from "@cosmjs/stargate"; -import { useWallet } from "@cosmos-kit/react"; import { useCallback } from "react"; +import { useCurrentChain } from "../hooks"; import { resendTx } from "lib/app-fns/tx/resend"; import type { HumanAddr } from "lib/types"; @@ -14,8 +14,7 @@ export interface ResendStreamParams { } export const useResendTx = () => { - const { address, getCosmWasmClient } = useWallet(); - + const { address, getSigningCosmWasmClient } = useCurrentChain(); return useCallback( async ({ onTxSucceed, @@ -23,7 +22,7 @@ export const useResendTx = () => { estimatedFee, messages, }: ResendStreamParams) => { - const client = await getCosmWasmClient(); + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); if (!estimatedFee) return null; @@ -36,6 +35,6 @@ export const useResendTx = () => { messages, }); }, - [address, getCosmWasmClient] + [address, getSigningCosmWasmClient] ); }; diff --git a/src/lib/app-provider/tx/submitProposal.ts b/src/lib/app-provider/tx/submitProposal.ts index 90c6db936..7083b0083 100644 --- a/src/lib/app-provider/tx/submitProposal.ts +++ b/src/lib/app-provider/tx/submitProposal.ts @@ -1,12 +1,15 @@ import type { EncodeObject } from "@cosmjs/proto-signing"; import type { StdFee } from "@cosmjs/stargate"; -import { useWallet } from "@cosmos-kit/react"; import { useCallback } from "react"; -import { submitProposalTx } from "lib/app-fns/tx/submitProposal"; +import { useCurrentChain } from "../hooks"; +import { + submitStoreCodeProposalTx, + submitWhitelistProposalTx, +} from "lib/app-fns/tx/submitProposal"; import type { HumanAddr } from "lib/types"; -export interface SubmitProposalStreamParams { +export interface SubmitWhitelistProposalStreamParams { onTxSucceed?: () => void; onTxFailed?: () => void; estimatedFee?: StdFee; @@ -15,8 +18,8 @@ export interface SubmitProposalStreamParams { amountToVote: string | null; } -export const useSubmitProposalTx = () => { - const { address, getCosmWasmClient } = useWallet(); +export const useSubmitWhitelistProposalTx = () => { + const { address, getSigningCosmWasmClient } = useCurrentChain(); return useCallback( async ({ @@ -26,12 +29,12 @@ export const useSubmitProposalTx = () => { messages, whitelistNumber, amountToVote, - }: SubmitProposalStreamParams) => { - const client = await getCosmWasmClient(); + }: SubmitWhitelistProposalStreamParams) => { + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); if (!estimatedFee) return null; - return submitProposalTx({ + return submitWhitelistProposalTx({ address: address as HumanAddr, client, onTxSucceed, @@ -42,6 +45,46 @@ export const useSubmitProposalTx = () => { amountToVote, }); }, - [address, getCosmWasmClient] + [address, getSigningCosmWasmClient] + ); +}; + +interface SubmitStoreCodeProposalStreamParams { + wasmFileName: string; + messages: EncodeObject[]; + amountToVote: string | null; + estimatedFee?: StdFee; + onTxSucceed?: () => void; + onTxFailed?: () => void; +} + +export const useSubmitStoreCodeProposalTx = () => { + const { address, getSigningCosmWasmClient, chain } = useCurrentChain(); + return useCallback( + async ({ + estimatedFee, + messages, + wasmFileName, + amountToVote, + onTxSucceed, + onTxFailed, + }: SubmitStoreCodeProposalStreamParams) => { + const client = await getSigningCosmWasmClient(); + if (!address || !client || !chain.chain_name) + throw new Error("Please check your wallet connection."); + if (!estimatedFee) return null; + return submitStoreCodeProposalTx({ + address: address as HumanAddr, + chainName: chain.chain_name, + client, + onTxSucceed, + onTxFailed, + fee: estimatedFee, + messages, + wasmFileName, + amountToVote, + }); + }, + [address, chain.chain_name, getSigningCosmWasmClient] ); }; diff --git a/src/lib/app-provider/tx/updateAdmin.ts b/src/lib/app-provider/tx/updateAdmin.ts index b0e8898a4..861e8aaad 100644 --- a/src/lib/app-provider/tx/updateAdmin.ts +++ b/src/lib/app-provider/tx/updateAdmin.ts @@ -1,7 +1,7 @@ import type { StdFee } from "@cosmjs/stargate"; -import { useWallet } from "@cosmos-kit/react"; import { useCallback } from "react"; +import { useCurrentChain } from "../hooks"; import { updateAdminTx } from "lib/app-fns/tx/updateAdmin"; import type { Addr, ContractAddr, HumanAddr, Option } from "lib/types"; @@ -14,7 +14,7 @@ export interface UpdateAdminStreamParams { } export const useUpdateAdminTx = () => { - const { address, getCosmWasmClient } = useWallet(); + const { address, getSigningCosmWasmClient } = useCurrentChain(); return useCallback( async ({ @@ -24,7 +24,7 @@ export const useUpdateAdminTx = () => { onTxSucceed, onTxFailed, }: UpdateAdminStreamParams) => { - const client = await getCosmWasmClient(); + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); if (!estimatedFee) return null; @@ -39,6 +39,6 @@ export const useUpdateAdminTx = () => { onTxFailed, }); }, - [address, getCosmWasmClient] + [address, getSigningCosmWasmClient] ); }; diff --git a/src/lib/app-provider/tx/upload.ts b/src/lib/app-provider/tx/upload.ts index 99dc592a9..028d3dd3d 100644 --- a/src/lib/app-provider/tx/upload.ts +++ b/src/lib/app-provider/tx/upload.ts @@ -1,38 +1,51 @@ import type { StdFee } from "@cosmjs/stargate"; -import { useWallet } from "@cosmos-kit/react"; +import { gzip } from "node-gzip"; import { useCallback } from "react"; +import { useCurrentChain } from "../hooks"; import { uploadContractTx } from "lib/app-fns/tx/upload"; -import type { HumanAddr, Option } from "lib/types"; +import type { AccessType, Addr, HumanAddr, Option } from "lib/types"; +import { composeStoreCodeMsg } from "lib/utils"; export interface UploadStreamParams { wasmFileName: Option; wasmCode: Option>; - codeDesc: string; + addresses: Addr[]; + permission: AccessType; + codeName: string; estimatedFee: Option; onTxSucceed?: (codeId: number) => void; } export const useUploadContractTx = (isMigrate: boolean) => { - const { address, getCosmWasmClient } = useWallet(); + const { address, getSigningCosmWasmClient } = useCurrentChain(); return useCallback( async ({ wasmFileName, wasmCode, - codeDesc, + addresses, + permission, + codeName, estimatedFee, onTxSucceed, }: UploadStreamParams) => { - const client = await getCosmWasmClient(); + const client = await getSigningCosmWasmClient(); if (!address || !client) throw new Error("Please check your wallet connection."); if (!wasmFileName || !wasmCode || !estimatedFee) return null; + const message = composeStoreCodeMsg({ + sender: address as Addr, + wasmByteCode: await gzip(new Uint8Array(await wasmCode)), + permission, + addresses, + }); + return uploadContractTx({ address: address as HumanAddr, - wasmCode: new Uint8Array(await wasmCode), - codeDesc, + messages: [message], + codeName, wasmFileName, fee: estimatedFee, client, @@ -40,6 +53,6 @@ export const useUploadContractTx = (isMigrate: boolean) => { isMigrate, }); }, - [address, getCosmWasmClient, isMigrate] + [address, getSigningCosmWasmClient, isMigrate] ); }; diff --git a/src/lib/app-provider/types.ts b/src/lib/app-provider/types.ts deleted file mode 100644 index 1cf756d89..000000000 --- a/src/lib/app-provider/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface AppConstants { - gasAdjustment: number; -} diff --git a/src/lib/chain-registry/localosmosis.ts b/src/lib/chain-registry/localosmosis.ts new file mode 100644 index 000000000..f91ce4652 --- /dev/null +++ b/src/lib/chain-registry/localosmosis.ts @@ -0,0 +1,81 @@ +import type { Chain, AssetList } from "@chain-registry/types"; + +export const localosmosis: Chain = { + $schema: "../chain.schema.json", + chain_name: "localosmosis", + status: "live", + network_type: "devnet", + pretty_name: "Local Osmosis", + chain_id: "localosmosis", + bech32_prefix: "osmo", + daemon_name: "osmosisd", + node_home: "$HOME/.osmosisd", + key_algos: ["secp256k1"], + slip44: 118, + fees: { + fee_tokens: [ + { + denom: "uosmo", + fixed_min_gas_price: 0.025, + low_gas_price: 0.025, + average_gas_price: 0.025, + high_gas_price: 0.025, + }, + ], + }, + staking: { + staking_tokens: [ + { + denom: "uosmo", + }, + ], + }, + apis: { + rpc: [ + { + address: "http://localhost/rpc/", + }, + ], + rest: [ + { + address: "http://localhost/rest", + }, + ], + }, + logo_URIs: { + png: "https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmosis-chain-logo.png", + }, + keywords: ["devnet"], +}; + +export const localosmosisAsset: AssetList = { + $schema: "../assetlist.schema.json", + chain_name: "localosmosis", + assets: [ + { + description: "The native token of Osmosis", + denom_units: [ + { + denom: "uosmo", + exponent: 0, + aliases: [], + }, + { + denom: "osmo", + exponent: 6, + aliases: [], + }, + ], + base: "uosmo", + name: "Osmosis", + display: "osmo", + symbol: "OSMO", + logo_URIs: { + png: "https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.png", + svg: "https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.svg", + }, + coingecko_id: "osmosis", + keywords: ["dex", "staking"], + }, + ], +}; diff --git a/src/lib/chain-registry/sei.ts b/src/lib/chain-registry/sei.ts new file mode 100644 index 000000000..02ffb0254 --- /dev/null +++ b/src/lib/chain-registry/sei.ts @@ -0,0 +1,115 @@ +import type { Chain, AssetList } from "@chain-registry/types"; + +export const sei: Chain = { + $schema: "../chain.schema.json", + chain_name: "sei", + status: "live", + website: "https://www.sei.io/", + network_type: "mainnet", + pretty_name: "Sei", + chain_id: "pacific-1", + bech32_prefix: "sei", + daemon_name: "seid", + node_home: "$HOME/.sei", + key_algos: ["secp256k1"], + slip44: 118, + fees: { + fee_tokens: [ + { + denom: "usei", + fixed_min_gas_price: 0, + low_gas_price: 0, + average_gas_price: 0.025, + high_gas_price: 0.04, + }, + ], + }, + staking: { + staking_tokens: [ + { + denom: "usei", + }, + ], + }, + codebase: { + git_repo: "https://github.com/sei-protocol/sei-chain", + recommended_version: "v3.0.0", + compatible_versions: ["v3.0.0"], + cosmos_sdk_version: "", + cosmwasm_enabled: true, + genesis: { + genesis_url: "", + }, + versions: [ + { + name: "v1", + }, + ], + }, + logo_URIs: { + png: "", + svg: "", + }, + peers: { + seeds: [ + { + id: "20e1000e88125698264454a884812746c2eb4807", + address: "seeds.lavenderfive.com:11956", + }, + ], + persistent_peers: [ + { + id: "20e1000e88125698264454a884812746c2eb4807", + address: "seeds.lavenderfive.com:11956", + }, + ], + }, + apis: { + rpc: [ + { + address: "https://sei-rpc.lavenderfive.com:443", + }, + ], + rest: [ + { + address: "https://sei-api.lavenderfive.com:443", + provider: "Lavender.Five Nodes 🐝", + }, + ], + grpc: [ + { + address: "https://sei-grpc.lavenderfive.com:443", + provider: "Lavender.Five Nodes 🐝", + }, + ], + }, + explorers: [ + { + kind: "ping.pub", + url: "https://ping.pub/sei", + }, + ], +}; +export const seiAssets: AssetList = { + $schema: "../assetlist.schema.json", + chain_name: "sei", + assets: [ + { + description: "The native staking token of Sei.", + denom_units: [ + { + denom: "usei", + exponent: 0, + }, + { + denom: "sei", + exponent: 6, + }, + ], + base: "usei", + name: "Sei", + display: "sei", + symbol: "SEI", + }, + ], +}; diff --git a/src/lib/components/AccordionStepperItem.tsx b/src/lib/components/AccordionStepperItem.tsx index 84ee343d1..f311c1dbf 100644 --- a/src/lib/components/AccordionStepperItem.tsx +++ b/src/lib/components/AccordionStepperItem.tsx @@ -1,27 +1,30 @@ +import type { FlexProps } from "@chakra-ui/react"; import { Flex } from "@chakra-ui/react"; +const AccordionStepperItemLine = (props: FlexProps) => ( + +); + export const AccordionStepperItem = () => ( - + + ); diff --git a/src/lib/components/AddressInput.tsx b/src/lib/components/AddressInput.tsx index 7b9795b9b..996f383f3 100644 --- a/src/lib/components/AddressInput.tsx +++ b/src/lib/components/AddressInput.tsx @@ -20,6 +20,7 @@ interface AddressInputProps validation?: RegisterOptions["validate"]; maxLength?: number; helperAction?: ReactNode; + requiredText?: string; } const getAddressStatus = (input: string, error: Option): FormStatus => { @@ -40,9 +41,12 @@ export const AddressInput = ({ validation = {}, maxLength, helperAction, + requiredText = "Address is empty", }: AddressInputProps) => { const { - appHumanAddress: { example: exampleAddr }, + chainConfig: { + exampleAddresses: { user: exampleAddr }, + }, } = useCelatoneApp(); const { validateUserAddress, validateContractAddress } = useValidateAddress(); const validateAddress = useCallback( @@ -73,7 +77,7 @@ export const AddressInput = ({ helperText={helperText} size={size} rules={{ - required: "Address is empty", + required: requiredText, validate: { validateAddress, ...validation }, }} maxLength={maxLength} diff --git a/src/lib/components/AppLink.tsx b/src/lib/components/AppLink.tsx index 92fef138a..a3a87f86d 100644 --- a/src/lib/components/AppLink.tsx +++ b/src/lib/components/AppLink.tsx @@ -2,23 +2,31 @@ import { Text } from "@chakra-ui/react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { DEFAULT_SUPPORTED_CHAIN_ID, SUPPORTED_CHAIN_IDS } from "env"; +import { getFirstQueryParam } from "lib/utils"; + export const AppLink = ({ children, ...linkProps }: React.ComponentProps) => { const router = useRouter(); const componentHref = linkProps.href.toString(); + + const network = SUPPORTED_CHAIN_IDS.includes( + getFirstQueryParam(router.query.network) + ) + ? router.query.network + : DEFAULT_SUPPORTED_CHAIN_ID; + return ( - + {typeof children === "string" ? ( - + {children} ) : ( diff --git a/src/lib/components/AssignMe.tsx b/src/lib/components/AssignMe.tsx index f1bedab86..d5b60b161 100644 --- a/src/lib/components/AssignMe.tsx +++ b/src/lib/components/AssignMe.tsx @@ -1,21 +1,29 @@ import { Text } from "@chakra-ui/react"; -import { useWallet } from "@cosmos-kit/react"; +import type { TextProps } from "@chakra-ui/react"; import type { MouseEventHandler } from "react"; -interface AssginMeProps { +import { useCurrentChain } from "lib/app-provider"; + +interface AssignMeProps { onClick?: MouseEventHandler; isDisable?: boolean; + textAlign?: TextProps["textAlign"]; } -export const AssignMe = ({ onClick, isDisable = false }: AssginMeProps) => { - const { address: walletAddress } = useWallet(); +export const AssignMe = ({ + onClick, + isDisable = false, + textAlign = "right", +}: AssignMeProps) => { + const { address: walletAddress } = useCurrentChain(); const enabled = Boolean(!isDisable && walletAddress); + return ( ; + href?: string; +}; + +type BreadcrumbProps = { + items: BreadcrumbItemProps[]; + mb?: number; +}; + +export const Breadcrumb = ({ items, mb = 0 }: BreadcrumbProps) => ( + + } + > + {items.map((item) => + item.href ? ( + item.text && ( + + + {item.text} + + + ) + ) : ( + + + {item.text} + + + ) + )} + +); diff --git a/src/lib/components/ButtonCard.tsx b/src/lib/components/ButtonCard.tsx index 85d5503ec..38d69cefa 100644 --- a/src/lib/components/ButtonCard.tsx +++ b/src/lib/components/ButtonCard.tsx @@ -20,18 +20,18 @@ export const ButtonCard = ({ }: ButtonCardProps) => ( - + ); diff --git a/src/lib/components/ConnectWalletAlert.tsx b/src/lib/components/ConnectWalletAlert.tsx index 57eacf0a5..4b4a07b82 100644 --- a/src/lib/components/ConnectWalletAlert.tsx +++ b/src/lib/components/ConnectWalletAlert.tsx @@ -7,27 +7,29 @@ import { Button, } from "@chakra-ui/react"; import type { AlertProps } from "@chakra-ui/react"; -import { useWallet } from "@cosmos-kit/react"; import type { MouseEventHandler } from "react"; -import { AmpEvent, AmpTrack } from "lib/services/amplitude"; +import { useCurrentChain } from "lib/app-provider"; +import { AmpTrackUseClickWallet } from "lib/services/amplitude"; import { CustomIcon } from "./icon"; interface ConnectWalletAlertProps extends AlertProps { title?: string; subtitle?: string; + page?: string; } export const ConnectWalletAlert = ({ title, subtitle, + page, ...alertProps }: ConnectWalletAlertProps) => { - const { address, connect } = useWallet(); + const { address, connect } = useCurrentChain(); const onClickConnect: MouseEventHandler = async (e) => { - AmpTrack(AmpEvent.USE_CLICK_WALLET); + AmpTrackUseClickWallet(page, "alert"); e.preventDefault(); await connect(); }; @@ -35,19 +37,19 @@ export const ConnectWalletAlert = ({ return !address ? ( - + {title} {subtitle} - diff --git a/src/lib/components/ContractCmdButton.tsx b/src/lib/components/ContractCmdButton.tsx index 5d741f151..5d1153244 100644 --- a/src/lib/components/ContractCmdButton.tsx +++ b/src/lib/components/ContractCmdButton.tsx @@ -12,9 +12,8 @@ export const ContractCmdButton = ({ variant="command-button" fontSize="12px" height="24px" - px="10px" borderRadius="16px" - fontWeight="400" + fontWeight={400} onClick={onClickCmd} > {cmd} diff --git a/src/lib/components/ContractSelectSection.tsx b/src/lib/components/ContractSelectSection.tsx index 58766bfef..72682e755 100644 --- a/src/lib/components/ContractSelectSection.tsx +++ b/src/lib/components/ContractSelectSection.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; import { useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; -import { useCelatoneApp, useLCDEndpoint, useMobile } from "lib/app-provider"; +import { useBaseApiRoute, useCelatoneApp, useMobile } from "lib/app-provider"; import { useContractStore } from "lib/providers/store"; import { queryInstantiateInfo } from "lib/services/contract"; import type { ContractLocalInfo } from "lib/stores/contract"; @@ -92,7 +92,9 @@ const ContractDetailsButton = ({ instantiator, label, }: ContractDetailsButtonProps) => { + const isMobile = useMobile(); const isExist = !!contractLocalInfo?.lists; + if (isMobile) return null; return isExist ? ( - queryInstantiateInfo(endpoint, indexerGraphClient, contractAddress), + queryInstantiateInfo(lcdEndpoint, indexerGraphClient, contractAddress), { enabled: false, retry: false, @@ -186,7 +188,7 @@ export const ContractSelectSection = observer( label: contractLocalInfo.label, }); } - }, [contractAddress, contractLocalInfo, endpoint, reset, refetch]); + }, [contractAddress, contractLocalInfo, lcdEndpoint, reset, refetch]); const contractState = watch(); const notSelected = contractAddress.length === 0; @@ -196,16 +198,19 @@ export const ContractSelectSection = observer( - - + + Contract Address {!notSelected ? ( )} - + Contract Name - + {mode === "all-lists" && contractState.isValid && ( { - const { address } = useWallet(); +export const CopyLink = ({ + value, + amptrackSection, + type, + showCopyOnHover = false, + ...flexProps +}: CopyLinkProps) => { + const { address } = useCurrentChain(); const { onCopy, hasCopied } = useClipboard(value); const [isHover, setIsHover] = useState(false); + + // TODO - Refactor + const displayIcon = useMemo(() => { + if (showCopyOnHover) { + if (isHover) { + return "flex"; + } + return "none"; + } + return undefined; + }, [showCopyOnHover, isHover]); + return ( { > { AmpTrackCopier(amptrackSection, type); onCopy(); }} _hover={{ textDecoration: "underline", - textDecorationColor: "lilac.light", - "& > p": { color: "lilac.light" }, + textDecorationColor: "secondary.light", + "& > p": { color: "secondary.light" }, }} cursor="pointer" onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)} + {...flexProps} > {value === address ? `${value} (Me)` : value} diff --git a/src/lib/components/CustomTab.tsx b/src/lib/components/CustomTab.tsx index 383fdbfea..96ef8ed87 100644 --- a/src/lib/components/CustomTab.tsx +++ b/src/lib/components/CustomTab.tsx @@ -2,7 +2,7 @@ import type { TabProps } from "@chakra-ui/react"; import { Button, useTab, Badge, useMultiStyleConfig } from "@chakra-ui/react"; interface CustomTabProps extends TabProps { - count?: number; + count?: number | string; } export const CustomTab = ({ count, ...restProps }: CustomTabProps) => { @@ -16,14 +16,17 @@ export const CustomTab = ({ count, ...restProps }: CustomTabProps) => { display="flex" alignItems="center" fontSize="14px" - fontWeight="700" + fontWeight={700} lineHeight="24px" letterSpacing="0.4px" variant="ghost-gray" mb={0} sx={{ "&[aria-selected=true]": { - color: "violet.light", + color: "primary.light", + }, + "&[aria-selected=false]": { + color: "gray.500", }, }} _active={{ @@ -34,7 +37,7 @@ export const CustomTab = ({ count, ...restProps }: CustomTabProps) => { {tabProps.children} {count !== undefined && ( - + {count} )} diff --git a/src/lib/components/DotSeparator.tsx b/src/lib/components/DotSeparator.tsx index 73441a265..0ffeb9d48 100644 --- a/src/lib/components/DotSeparator.tsx +++ b/src/lib/components/DotSeparator.tsx @@ -1,5 +1,5 @@ import { Box } from "@chakra-ui/react"; export const DotSeparator = () => ( - + ); diff --git a/src/lib/components/DotSeperator.tsx b/src/lib/components/DotSeperator.tsx deleted file mode 100644 index 73441a265..000000000 --- a/src/lib/components/DotSeperator.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Box } from "@chakra-ui/react"; - -export const DotSeparator = () => ( - -); diff --git a/src/lib/components/ErrorMessageRender.tsx b/src/lib/components/ErrorMessageRender.tsx index 7d67ee009..09d1bb997 100644 --- a/src/lib/components/ErrorMessageRender.tsx +++ b/src/lib/components/ErrorMessageRender.tsx @@ -12,7 +12,7 @@ export const ErrorMessageRender = ({ ...restProps }: ErrorMessageRenderProps) => ( - + {error} diff --git a/src/lib/components/EstimatedFeeRender.tsx b/src/lib/components/EstimatedFeeRender.tsx index 69451afeb..edcea89eb 100644 --- a/src/lib/components/EstimatedFeeRender.tsx +++ b/src/lib/components/EstimatedFeeRender.tsx @@ -1,6 +1,7 @@ import { Spinner } from "@chakra-ui/react"; import type { StdFee } from "@cosmjs/stargate"; +import { useAssetInfos } from "lib/services/assetService"; import { formatBalanceWithDenom } from "lib/utils"; export const EstimatedFeeRender = ({ @@ -10,7 +11,8 @@ export const EstimatedFeeRender = ({ estimatedFee: StdFee | undefined; loading: boolean; }) => { - if (loading) { + const { assetInfos, isLoading } = useAssetInfos(); + if (loading || isLoading) { return ( <> Estimating ... @@ -21,5 +23,15 @@ export const EstimatedFeeRender = ({ if (!coin) return <>--; - return <>{formatBalanceWithDenom({ coin, precision: 6 })}; + const chainAssetInfo = assetInfos?.[coin.denom]; + + return ( + <> + {formatBalanceWithDenom({ + coin, + precision: chainAssetInfo?.precision, + symbol: chainAssetInfo?.symbol, + })} + + ); }; diff --git a/src/lib/components/Expedited.tsx b/src/lib/components/Expedited.tsx index 9dac00cd2..e4423319c 100644 --- a/src/lib/components/Expedited.tsx +++ b/src/lib/components/Expedited.tsx @@ -23,13 +23,13 @@ export const Expedited = ({ isActiveExpedited }: ExpeditedProps) => ( Expedited diff --git a/src/lib/components/ExplorerLink.tsx b/src/lib/components/ExplorerLink.tsx index 64064b1e4..a8b082468 100644 --- a/src/lib/components/ExplorerLink.tsx +++ b/src/lib/components/ExplorerLink.tsx @@ -1,13 +1,12 @@ import type { BoxProps, TextProps } from "@chakra-ui/react"; -import { Box, Text } from "@chakra-ui/react"; -import { useWallet } from "@cosmos-kit/react"; +import { Box, Text, Flex } from "@chakra-ui/react"; -import { - getExplorerProposalUrl, - getExplorerValidatorUrl, -} from "lib/app-fns/explorer"; +import type { ExplorerConfig } from "config/types"; import type { AddressReturnType } from "lib/app-provider"; -import { useCurrentNetwork } from "lib/app-provider/hooks/useCurrentNetwork"; +import { useCelatoneApp } from "lib/app-provider/contexts"; +import { useBaseApiRoute } from "lib/app-provider/hooks/useBaseApiRoute"; +import { useCurrentChain } from "lib/app-provider/hooks/useCurrentChain"; +import { useMobile } from "lib/app-provider/hooks/useMediaQuery"; import { AmpTrackMintscan } from "lib/services/amplitude"; import type { Option } from "lib/types"; import { truncate } from "lib/utils"; @@ -20,7 +19,8 @@ export type LinkType = | "tx_hash" | "code_id" | "block_height" - | "proposal_id"; + | "proposal_id" + | "pool_id"; interface ExplorerLinkProps extends BoxProps { value: string; @@ -33,12 +33,14 @@ interface ExplorerLinkProps extends BoxProps { textVariant?: TextProps["variant"]; ampCopierSection?: string; openNewTab?: boolean; + fixedHeight?: boolean; } -const getNavigationUrl = ( +export const getNavigationUrl = ( type: ExplorerLinkProps["type"], - currentChainName: string, - value: string + explorerConfig: ExplorerConfig, + value: string, + lcdEndpoint: string ) => { let url = ""; switch (type) { @@ -52,7 +54,9 @@ const getNavigationUrl = ( url = "/accounts"; break; case "validator_address": - url = getExplorerValidatorUrl(currentChainName); + url = + explorerConfig.validator || + `${lcdEndpoint}/cosmos/staking/v1beta1/validators`; break; case "code_id": url = "/codes"; @@ -61,7 +65,12 @@ const getNavigationUrl = ( url = "/blocks"; break; case "proposal_id": - url = getExplorerProposalUrl(currentChainName); + url = + explorerConfig.proposal || + `${lcdEndpoint}/cosmos/gov/v1beta1/proposals`; + break; + case "pool_id": + url = "/pools"; break; case "invalid_address": return ""; @@ -85,7 +94,7 @@ const getValueText = ( const getCopyLabel = (type: LinkType) => type .split("_") - .map((str) => str.charAt(0).toUpperCase() + str.slice(1)) + .map((str: string) => str.charAt(0).toUpperCase() + str.slice(1)) .join(" "); const LinkRender = ({ @@ -107,16 +116,19 @@ const LinkRender = ({ textVariant: TextProps["variant"]; openNewTab: Option; }) => { - const { network } = useCurrentNetwork(); + const { currentChainId } = useCelatoneApp(); const textElement = ( {textValue} @@ -128,7 +140,7 @@ const LinkRender = ({ ) : ( { - const { address, currentChainName } = useWallet(); + const { address } = useCurrentChain(); + const lcdEndpoint = useBaseApiRoute("rest"); + const { + chainConfig: { explorerLink: explorerConfig }, + } = useCelatoneApp(); + const isInternal = type === "code_id" || type === "contract_address" || type === "user_address" || type === "tx_hash" || - type === "block_height"; + type === "block_height" || + type === "pool_id"; const [hrefLink, textValue] = [ - getNavigationUrl(type, currentChainName, copyValue || value), + getNavigationUrl(type, explorerConfig, copyValue || value, lcdEndpoint), getValueText(value === address, textFormat === "truncate", value), ]; const readOnly = isReadOnly || !hrefLink; - + const isMobile = useMobile(); return ( {readOnly ? ( - {textValue} + + {textValue} + ) : ( - <> + - + )} ); diff --git a/src/lib/components/InputWithIcon.tsx b/src/lib/components/InputWithIcon.tsx index 1cf789a65..402cf8f9e 100644 --- a/src/lib/components/InputWithIcon.tsx +++ b/src/lib/components/InputWithIcon.tsx @@ -29,8 +29,8 @@ const InputWithIcon = ({ size={size} onClick={action ? () => AmpTrack(AmpEvent.USE_SEARCH_INPUT) : undefined} /> - - + + ); diff --git a/src/lib/components/LabelText.tsx b/src/lib/components/LabelText.tsx index ac697451a..825b8edc1 100644 --- a/src/lib/components/LabelText.tsx +++ b/src/lib/components/LabelText.tsx @@ -1,8 +1,11 @@ import type { FlexProps } from "@chakra-ui/react"; import { Flex, Text } from "@chakra-ui/react"; +import { TooltipInfo } from "./Tooltip"; + interface LabelTextProps extends FlexProps { label: string; + tooltipText?: string; children: string | JSX.Element; helperText1?: string; helperText2?: string; @@ -10,15 +13,19 @@ interface LabelTextProps extends FlexProps { export const LabelText = ({ label, + tooltipText, children, helperText1, helperText2, ...flexProps }: LabelTextProps) => ( - - {label} - + + + {label} + + {tooltipText && } + {typeof children === "string" ? ( {children} ) : ( diff --git a/src/lib/components/ListSelection.tsx b/src/lib/components/ListSelection.tsx index 0de9d2e30..2edfae202 100644 --- a/src/lib/components/ListSelection.tsx +++ b/src/lib/components/ListSelection.tsx @@ -46,7 +46,7 @@ export const ListSelection = forwardRef( setResult, placeholder, helperText, - labelBgColor = "pebble.900", + labelBgColor = "gray.900", ...rest }: ListSelectionProps, ref @@ -127,14 +127,14 @@ export const ListSelection = forwardRef( alignItems="center" color="text.main" border="1px solid" - borderColor="pebble.700" + borderColor="gray.700" background="none" borderRadius="8px" maxW="100%" overflowX="scroll" > {result.length > 0 && ( - + {[...result].map((option) => ( ( {option.label} - + ))} @@ -175,7 +175,7 @@ export const ListSelection = forwardRef( position="absolute" top={0} left={0} - fontWeight="400" + fontWeight={400} color="text.dark" bgColor={labelBgColor} pointerEvents="none" @@ -194,9 +194,9 @@ export const ListSelection = forwardRef( {displayOptions && ( ( selectOptionFromList(option)} > @@ -221,8 +221,8 @@ export const ListSelection = forwardRef( data-label={option.label} mr={2} name="check" - color="pebble.600" - boxSize="3" + color="gray.600" + boxSize={3} /> )} @@ -235,13 +235,13 @@ export const ListSelection = forwardRef( setEnableOutside(false)} > - + Create New List diff --git a/src/lib/components/Loading.tsx b/src/lib/components/Loading.tsx index 961308b1e..a2c56f28d 100644 --- a/src/lib/components/Loading.tsx +++ b/src/lib/components/Loading.tsx @@ -1,16 +1,20 @@ import { Flex, Spinner, Text } from "@chakra-ui/react"; -export const Loading = () => ( +interface LoadingProps { + withBorder?: boolean; +} + +export const Loading = ({ withBorder = true }: LoadingProps) => ( - Loading ... + Loading ... ); diff --git a/src/lib/components/Meta.tsx b/src/lib/components/Meta.tsx index 7df3f346e..4723bd7a8 100644 --- a/src/lib/components/Meta.tsx +++ b/src/lib/components/Meta.tsx @@ -1,10 +1,13 @@ -import { SELECTED_CHAIN } from "env"; +import { CURR_THEME } from "env"; +import { useCelatoneApp } from "lib/app-provider"; -const APP_NAME = "celatone"; +const APP_NAME = CURR_THEME.branding.seo.appName; const Meta = () => { - const chainName = SELECTED_CHAIN || ""; - const title = `${chainName.charAt(0).toUpperCase() + chainName.slice(1)}`; + const { + chainConfig: { prettyName }, + } = useCelatoneApp(); + const title = `${prettyName} Explorer | ${CURR_THEME.branding.seo.title}`; return ( <> @@ -13,36 +16,30 @@ const Meta = () => { - - {`${title} Explorer | Celatone`} - + + {title} + {/* Open Graph / Facebook */} - + - + {/* Twitter */} - - + + ); }; diff --git a/src/lib/components/MobileGuard.tsx b/src/lib/components/MobileGuard.tsx index 344085f78..9c71cbb52 100644 --- a/src/lib/components/MobileGuard.tsx +++ b/src/lib/components/MobileGuard.tsx @@ -1,6 +1,7 @@ +import { useRouter } from "next/router"; import type { ReactNode } from "react"; -import { useMobile } from "lib/app-provider"; +import { useCelatoneApp, useMobile } from "lib/app-provider"; import { NoMobile } from "./modal"; @@ -8,6 +9,25 @@ interface MobileGuardProps { children: ReactNode; } export const MobileGuard = ({ children }: MobileGuardProps) => { + const router = useRouter(); + const pathName = router.asPath; const isMobile = useMobile(); - return isMobile ? : <>{children}; + const { currentChainId } = useCelatoneApp(); + const isResponsive = + pathName.includes(`account`) || + pathName.includes(`/txs`) || + pathName.includes(`/blocks`) || + pathName.includes(`/contracts/`) || + pathName.includes(`/projects`) || + pathName.includes(`/code`) || + pathName.includes(`/query`) || + pathName.includes(`/network-overview`) || + pathName.includes(`/dev-home`) || + pathName.includes(`/404`) || + pathName === `/${currentChainId}/contracts` || + pathName === `/${currentChainId}`; + + if (isResponsive && isMobile) return <>{children}; + if (!isResponsive && isMobile) return ; + return <>{children}; }; diff --git a/src/lib/components/OffChainForm.tsx b/src/lib/components/OffChainForm.tsx index dc301e854..3e3e42607 100644 --- a/src/lib/components/OffChainForm.tsx +++ b/src/lib/components/OffChainForm.tsx @@ -1,12 +1,8 @@ import { VStack } from "@chakra-ui/react"; import type { Control, FieldErrorsImpl, FieldPath } from "react-hook-form"; -import { - getMaxContractDescriptionLengthError, - getMaxContractNameLengthError, - MAX_CONTRACT_DESCRIPTION_LENGTH, - MAX_CONTRACT_NAME_LENGTH, -} from "lib/data"; +import { useCelatoneApp } from "lib/app-provider"; +import { useGetMaxLengthError } from "lib/hooks"; import type { LVPair } from "lib/types"; import { ControllerInput, ControllerTextarea } from "./forms"; @@ -38,50 +34,57 @@ export const OffChainForm = ({ setContractListsValue, errors, labelBgColor = "background.main", -}: OffChainFormProps) => ( - - } - control={control} - label="Name" - placeholder={contractLabel} - helperText="Set name for your contract" - variant="floating" - rules={{ - maxLength: MAX_CONTRACT_NAME_LENGTH, - }} - error={errors.name && getMaxContractNameLengthError(state.name.length)} - labelBgColor={labelBgColor} - /> - } - control={control} - label="Description" - placeholder="Help understanding what this contract do and how it works ..." - variant="floating" - rules={{ - maxLength: MAX_CONTRACT_DESCRIPTION_LENGTH, - }} - error={ - errors.description && - getMaxContractDescriptionLengthError(state.description.length) - } - labelBgColor={labelBgColor} - /> - - + } + control={control} + label="Name" + placeholder={contractLabel} + helperText="Set name for your contract" + variant="floating" + rules={{ + maxLength: constants.maxContractNameLength, + }} + error={ + errors.name && getMaxLengthError(state.name.length, "contract_name") + } + labelBgColor={labelBgColor} + /> + } + control={control} + label="Description" + placeholder="Help understanding what this contract do and how it works ..." + variant="floating" + rules={{ + maxLength: constants.maxContractDescriptionLength, + }} + error={ + errors.description && + getMaxLengthError(state.description.length, "contract_desc") + } + labelBgColor={labelBgColor} + /> + + - -); + setResult={setContractListsValue} + labelBgColor={labelBgColor} + /> + + ); +}; diff --git a/src/lib/components/PageContainer.tsx b/src/lib/components/PageContainer.tsx index f4b8cd380..7bc305562 100644 --- a/src/lib/components/PageContainer.tsx +++ b/src/lib/components/PageContainer.tsx @@ -6,7 +6,12 @@ type PageContainerProps = { }; const PageContainer = ({ children }: PageContainerProps) => ( - + {children} ); diff --git a/src/lib/components/PermissionChip.tsx b/src/lib/components/PermissionChip.tsx index 740b37bb8..c41748d76 100644 --- a/src/lib/components/PermissionChip.tsx +++ b/src/lib/components/PermissionChip.tsx @@ -1,26 +1,33 @@ import { Flex, Tag } from "@chakra-ui/react"; -import { useWallet } from "@cosmos-kit/react"; -import type { HumanAddr, PermissionAddresses } from "lib/types"; -import { AccessConfigPermission } from "lib/types"; -import { getPermissionHelper } from "lib/utils"; +import { useCurrentChain } from "lib/app-provider"; +import type { + HumanAddr, + PermissionAddresses, + AccessConfigPermission, +} from "lib/types"; +import { getPermissionHelper, resolvePermission } from "lib/utils"; import { Tooltip } from "./Tooltip"; interface PermissionChipProps { instantiatePermission: AccessConfigPermission; permissionAddresses: PermissionAddresses; + tagSize?: string; } export const PermissionChip = ({ instantiatePermission, permissionAddresses, + tagSize = "md", }: PermissionChipProps) => { - const { address } = useWallet(); + const { address } = useCurrentChain(); - const isAllowed = - permissionAddresses.includes(address as HumanAddr) || - instantiatePermission === AccessConfigPermission.EVERYBODY; + const isAllowed = resolvePermission( + address as HumanAddr, + instantiatePermission, + permissionAddresses + ); const { message } = getPermissionHelper( address as HumanAddr, @@ -30,8 +37,8 @@ export const PermissionChip = ({ return ( - - + e.stopPropagation()} w="fit-content"> + {instantiatePermission} diff --git a/src/lib/components/PublicDescription.tsx b/src/lib/components/PublicDescription.tsx index 920bb5967..b8f831acb 100644 --- a/src/lib/components/PublicDescription.tsx +++ b/src/lib/components/PublicDescription.tsx @@ -30,16 +30,16 @@ export const PublicDescription = ({ return ( {icon} - + {title} diff --git a/src/lib/components/Seo.tsx b/src/lib/components/Seo.tsx index 6d10286a0..2442cdbac 100644 --- a/src/lib/components/Seo.tsx +++ b/src/lib/components/Seo.tsx @@ -1,20 +1,24 @@ -import { useWallet } from "@cosmos-kit/react"; import { DefaultSeo } from "next-seo"; +import { CURR_THEME } from "env"; +import { useCelatoneApp } from "lib/app-provider"; + export const CelatoneSeo = () => { - const { currentChainRecord } = useWallet(); - const title = `${currentChainRecord?.chain.pretty_name} Explorer | Celatone`; + const { + chainConfig: { prettyName }, + } = useCelatoneApp(); + const title = `${prettyName} Explorer | ${CURR_THEME.branding.seo.title}`; return ( { ], }} twitter={{ - handle: "@celatone_", - cardType: "summary_large_image", + handle: CURR_THEME.branding.seo.twitter.handle, + cardType: CURR_THEME.branding.seo.twitter.cardType, }} /> ); diff --git a/src/lib/components/StickySidebar.tsx b/src/lib/components/StickySidebar.tsx index 12cfb2c39..732654c3b 100644 --- a/src/lib/components/StickySidebar.tsx +++ b/src/lib/components/StickySidebar.tsx @@ -10,84 +10,103 @@ import { Text, } from "@chakra-ui/react"; -import { useCurrentNetwork, useInternalNavigate } from "lib/app-provider"; -import type { Network } from "lib/data"; +import { useInternalNavigate } from "lib/app-provider"; +import { AmpTrackUseRightHelperPanel } from "lib/services/amplitude"; import { CustomIcon } from "./icon"; export interface SidebarMetadata { + page: string; title: string; description: React.ReactElement; - toStoreCode?: boolean; + toPagePath?: string; + toPageTitle?: string; + toPage?: boolean; } -export type SidebarDetails = Record; - interface StickySidebarProps extends BoxProps { - metadata: SidebarDetails; + metadata: SidebarMetadata; +} + +interface ToPageProps { + onClick: () => void; + title: string; } +const ToPage = ({ onClick, title }: ToPageProps) => ( + + + {title} + + + +); export const StickySidebar = ({ metadata, ...boxProps }: StickySidebarProps) => { const navigate = useInternalNavigate(); - const { network } = useCurrentNetwork(); - const { title, description, toStoreCode } = metadata[network]; - const hasAction = toStoreCode; + const { title, description, toPagePath, toPageTitle, toPage, page } = + metadata; + const hasAction = toPage; return ( - - + + {title} - + - + {description} - {toStoreCode && ( - { + AmpTrackUseRightHelperPanel(page, `to-${toPagePath}`); + navigate({ pathname: toPagePath }); }} - onClick={() => - navigate({ pathname: "/proposals/store-code" }) - } - > - - Submit Proposal To Store Code - - - + title={toPageTitle} + /> )} diff --git a/src/lib/components/TagSelection.tsx b/src/lib/components/TagSelection.tsx index fd5d3f5c5..b5c9d62c2 100644 --- a/src/lib/components/TagSelection.tsx +++ b/src/lib/components/TagSelection.tsx @@ -141,11 +141,11 @@ export const TagSelection = observer( borderRadius="8px" maxW="100%" border="1px solid" - borderColor="pebble.700" + borderColor="gray.700" overflowX="scroll" > {result.length > 0 && ( - + {result.map((option) => ( {option} - + ))} @@ -182,7 +182,7 @@ export const TagSelection = observer( position="absolute" top={0} left={0} - fontWeight="400" + fontWeight={400} color="text.dark" bgColor={labelBgColor} pointerEvents="none" @@ -201,9 +201,9 @@ export const TagSelection = observer( {displayOptions && ( {noResultAndUncreatable ? ( @@ -233,7 +233,7 @@ export const TagSelection = observer( selectOptionFromList(option)} > @@ -246,8 +246,8 @@ export const TagSelection = observer( data-label={option} mr={2} name="check" - color="pebble.600" - boxSize="3" + color="gray.600" + boxSize={3} /> )} @@ -257,7 +257,7 @@ export const TagSelection = observer( {canCreateOption && inputValue && ( createOption()} diff --git a/src/lib/components/ToggleWithName.tsx b/src/lib/components/ToggleWithName.tsx new file mode 100644 index 000000000..2273fa0c7 --- /dev/null +++ b/src/lib/components/ToggleWithName.tsx @@ -0,0 +1,31 @@ +import { Button, Flex } from "@chakra-ui/react"; + +import type { LVPair } from "lib/types"; + +interface ToggleWithNameProps { + selectedValue: string; + options: LVPair[]; + selectOption: (value: string) => void; +} + +export const ToggleWithName = ({ + selectedValue, + options, + selectOption, +}: ToggleWithNameProps) => ( + + {options.map((item) => ( + + ))} + +); diff --git a/src/lib/components/TokenCard.tsx b/src/lib/components/TokenCard.tsx index 671e72a47..d2af5f3ea 100644 --- a/src/lib/components/TokenCard.tsx +++ b/src/lib/components/TokenCard.tsx @@ -25,19 +25,14 @@ export const TokenCard = ({ const { symbol, price, amount, precision, id } = userBalance.balance; return ( - + @@ -45,7 +40,7 @@ export const TokenCard = ({ gap={1} alignItems="center" borderBottom="1px solid" - borderBottomColor="pebble.700" + borderBottomColor="gray.700" pb={2} > {symbol} - + {price ? formatPrice(price as USD) : "N/A"} - + {formatUTokenWithPrecision(amount as U, precision, false)} diff --git a/src/lib/components/Tooltip.tsx b/src/lib/components/Tooltip.tsx index f96c1395c..e110ff274 100644 --- a/src/lib/components/Tooltip.tsx +++ b/src/lib/components/Tooltip.tsx @@ -1,5 +1,7 @@ import type { TooltipProps } from "@chakra-ui/react"; -import { Tooltip as ChakraTooltip } from "@chakra-ui/react"; +import { Flex, Tooltip as ChakraTooltip } from "@chakra-ui/react"; + +import { CustomIcon } from "./icon"; export const Tooltip = ({ placement = "top", @@ -12,3 +14,23 @@ export const Tooltip = ({ {...tooltipProps} /> ); + +interface TooltipInfoProps extends Omit { + iconVariant?: "default" | "solid"; +} + +export const TooltipInfo = ({ + iconVariant = "default", + ...tooltipProps +}: TooltipInfoProps) => ( + + + + + +); diff --git a/src/lib/components/TxFilterSelection.tsx b/src/lib/components/TxFilterSelection.tsx index 72e169140..f7c1be62d 100644 --- a/src/lib/components/TxFilterSelection.tsx +++ b/src/lib/components/TxFilterSelection.tsx @@ -28,6 +28,8 @@ export interface TxFilterSelectionProps extends InputProps { label?: string; boxWidth?: LayoutProps["width"]; boxHeight?: LayoutProps["height"]; + size?: string | object; + tagSize?: string | object; } const listItemProps: CSSProperties = { @@ -49,9 +51,11 @@ export const TxFilterSelection = forwardRef< placeholder, helperText, labelBgColor = "background.main", - label = "Filter by Actions", + label = "Filter by Action", boxWidth = "full", boxHeight = "56px", + size = "lg", + tagSize = "md", ...rest }: TxFilterSelectionProps, ref @@ -112,12 +116,12 @@ export const TxFilterSelection = forwardRef< background="none" borderRadius="8px" border="1px solid" - borderColor="pebble.700" + borderColor="gray.700" maxW="100%" overflowX="scroll" > {result.length > 0 && ( - + {[...result].reverse().map((option) => ( {displayActionValue(option)} - + ))} @@ -143,7 +148,7 @@ export const TxFilterSelection = forwardRef< autoComplete="off" w="100%" minW="200px" - size="lg" + size={size} placeholder={result.length ? "" : placeholder} onChange={(e) => filterOptions(e.currentTarget.value)} onFocus={() => { @@ -159,7 +164,7 @@ export const TxFilterSelection = forwardRef< position="absolute" top={0} left={0} - fontWeight="400" + fontWeight={400} color="text.dark" bgColor={labelBgColor} pointerEvents="none" @@ -178,9 +183,9 @@ export const TxFilterSelection = forwardRef< {displayOptions && ( selectOption(option)} > {displayActionValue(option)} - {isOptionSelected(option) && ( )} diff --git a/src/lib/components/TxRelationSelection.tsx b/src/lib/components/TxRelationSelection.tsx index 5074ae45e..76dfd3dd6 100644 --- a/src/lib/components/TxRelationSelection.tsx +++ b/src/lib/components/TxRelationSelection.tsx @@ -30,25 +30,44 @@ const relationOptions = [ ]; interface TxRelationSelectionProps extends BoxProps { + value: Option; setValue: (value: Option) => void; + size?: string | object; } export const TxRelationSelection = ({ + value, setValue, + size = "lg", ...props -}: TxRelationSelectionProps) => ( - - - setValue( - value === RelationType.ALL - ? undefined - : value === RelationType.SIGNING - ) - } - initialSelected={RelationType.ALL} - /> - -); +}: TxRelationSelectionProps) => { + let initialValue; + switch (value) { + case undefined: + initialValue = RelationType.ALL; + break; + case false: + initialValue = RelationType.SIGNING; + break; + default: + initialValue = RelationType.RELATED; + } + + return ( + + + setValue( + newValue === RelationType.ALL + ? undefined + : newValue === RelationType.SIGNING + ) + } + initialSelected={initialValue} + /> + + ); +}; diff --git a/src/lib/components/ValidatorBadge.tsx b/src/lib/components/ValidatorBadge.tsx index c2463cb5e..a4e36a637 100644 --- a/src/lib/components/ValidatorBadge.tsx +++ b/src/lib/components/ValidatorBadge.tsx @@ -1,15 +1,19 @@ import type { ImageProps } from "@chakra-ui/react"; import { Flex, Image, Text } from "@chakra-ui/react"; -import { useWallet } from "@cosmos-kit/react"; -import { getChainApiPath } from "env"; +import { CURR_THEME } from "env"; +import { useCelatoneApp, useMobile } from "lib/app-provider"; import { ExplorerLink } from "lib/components/ExplorerLink"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; import type { ValidatorInfo } from "lib/types"; import { removeSpecialChars } from "lib/utils"; interface ValidatorBadgeProps { validator: ValidatorInfo | null; badgeSize?: ImageProps["boxSize"]; + ampCopierSection?: string; + maxWidth?: string; + hasLabel?: boolean; } const FallbackRender = ({ @@ -33,30 +37,43 @@ const FallbackRender = ({ export const ValidatorBadge = ({ validator, badgeSize = 10, + ampCopierSection, + maxWidth = "160px", + hasLabel = true, }: ValidatorBadgeProps) => { - const { currentChainName } = useWallet(); + const { + chainConfig: { chain }, + } = useCelatoneApp(); + const isMobile = useMobile(); return ( {validator ? ( <> {validator.moniker} - + + {isMobile && hasLabel && } + + ) : ( diff --git a/src/lib/components/ViewPermissionAddresses.tsx b/src/lib/components/ViewPermissionAddresses.tsx index 96ff89e09..144e89d88 100644 --- a/src/lib/components/ViewPermissionAddresses.tsx +++ b/src/lib/components/ViewPermissionAddresses.tsx @@ -35,13 +35,13 @@ export const ViewPermissionAddresses = ({ ))} {permissionAddresses.length > 1 && ( ); diff --git a/src/lib/components/button/ConnectWallet.tsx b/src/lib/components/button/ConnectWallet.tsx index 726484ecf..5f8d797c1 100644 --- a/src/lib/components/button/ConnectWallet.tsx +++ b/src/lib/components/button/ConnectWallet.tsx @@ -1,12 +1,12 @@ import { Button } from "@chakra-ui/react"; -import { useWallet } from "@cosmos-kit/react"; import type { MouseEventHandler } from "react"; import { CustomIcon } from "../icon"; +import { useCurrentChain } from "lib/app-provider"; import { AmpEvent, AmpTrack } from "lib/services/amplitude"; export const ConnectWalletBtn = () => { - const { connect } = useWallet(); + const { connect } = useCurrentChain(); const onClickConnect: MouseEventHandler = async (e) => { AmpTrack(AmpEvent.USE_CLICK_WALLET); diff --git a/src/lib/components/button/CustomIconButton.tsx b/src/lib/components/button/CustomIconButton.tsx index d84899e37..45c1192fd 100644 --- a/src/lib/components/button/CustomIconButton.tsx +++ b/src/lib/components/button/CustomIconButton.tsx @@ -17,7 +17,7 @@ export const CustomIconButton = ({ ); diff --git a/src/lib/components/button/FaucetButton.tsx b/src/lib/components/button/FaucetButton.tsx index 40677c32a..6a02a53a3 100644 --- a/src/lib/components/button/FaucetButton.tsx +++ b/src/lib/components/button/FaucetButton.tsx @@ -2,11 +2,17 @@ import { Button } from "@chakra-ui/react"; import type { MouseEventHandler } from "react"; import { CustomIcon } from "../icon"; -import { useCurrentNetwork, useInternalNavigate } from "lib/app-provider"; +import { useCelatoneApp, useInternalNavigate } from "lib/app-provider"; export const FaucetBtn = () => { const navigate = useInternalNavigate(); - const { isTestnet } = useCurrentNetwork(); + const { + chainConfig: { + features: { + faucet: { enabled }, + }, + }, + } = useCelatoneApp(); const onClick: MouseEventHandler = async (e) => { e.preventDefault(); @@ -15,7 +21,7 @@ export const FaucetBtn = () => { }); }; - return isTestnet ? ( + return enabled ? ( ); diff --git a/src/lib/components/button/ResendButton.tsx b/src/lib/components/button/ResendButton.tsx index c95cf014e..33b28effb 100644 --- a/src/lib/components/button/ResendButton.tsx +++ b/src/lib/components/button/ResendButton.tsx @@ -2,10 +2,14 @@ import { Button } from "@chakra-ui/react"; import type { EncodeObject } from "@cosmjs/proto-signing"; import { useCallback, useState } from "react"; -import { useFabricateFee, useResendTx, useSimulateFee } from "lib/app-provider"; +import { + useFabricateFee, + useResendTx, + useSimulateFeeQuery, +} from "lib/app-provider"; import { useTxBroadcast } from "lib/providers/tx-broadcast"; import { AmpEvent, AmpTrack } from "lib/services/amplitude"; -import type { Message, Msg } from "lib/types"; +import type { Gas, Message, Msg, Option } from "lib/types"; import { camelToSnake, encode } from "lib/utils"; interface ResendButtonProps { @@ -29,38 +33,45 @@ const formatMsgs = (messages: Message[]) => export const ResendButton = ({ messages }: ResendButtonProps) => { const fabricateFee = useFabricateFee(); - const { simulate } = useSimulateFee(); const resendTx = useResendTx(); const { broadcast } = useTxBroadcast(); const [isProcessing, setIsProcessing] = useState(false); + const composedMsgs = formatMsgs(messages); - const proceed = useCallback(async () => { - AmpTrack(AmpEvent.ACTION_RESEND); - const formatedMsgs = formatMsgs(messages); - const estimatedGasUsed = await simulate(formatedMsgs); + const proceed = useCallback( + async (estimatedGasUsed: Option>) => { + AmpTrack(AmpEvent.ACTION_RESEND); + const stream = await resendTx({ + onTxSucceed: () => setIsProcessing(false), + onTxFailed: () => setIsProcessing(false), + estimatedFee: estimatedGasUsed + ? fabricateFee(estimatedGasUsed) + : undefined, + messages: composedMsgs, + }); + if (stream) broadcast(stream); + }, + [broadcast, composedMsgs, fabricateFee, resendTx] + ); - const stream = await resendTx({ - onTxSucceed: () => setIsProcessing(false), - onTxFailed: () => setIsProcessing(false), - estimatedFee: estimatedGasUsed - ? fabricateFee(estimatedGasUsed) - : undefined, - messages: formatedMsgs, - }); - if (stream) broadcast(stream); - }, [broadcast, fabricateFee, messages, resendTx, simulate]); + const { isFetching: isSimulating } = useSimulateFeeQuery({ + enabled: isProcessing, + messages: composedMsgs, + onSuccess: (estimatedGasUsed) => proceed(estimatedGasUsed), + onError: () => setIsProcessing(false), + }); return ( diff --git a/src/lib/components/card/AccountCard.tsx b/src/lib/components/card/AccountCard.tsx new file mode 100644 index 000000000..b0a9d53c3 --- /dev/null +++ b/src/lib/components/card/AccountCard.tsx @@ -0,0 +1,53 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "../ExplorerLink"; +import { useInternalNavigate } from "lib/app-provider"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import { getNavigationArgs } from "lib/pages/public-project/components/table/account/PublicProjectAccountRow"; +import type { Account } from "lib/types"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +interface AccountCardProps { + accountInfo: Account; +} +export const AccountCard = ({ accountInfo }: AccountCardProps) => { + const navigate = useInternalNavigate(); + const goToDetail = () => { + navigate(getNavigationArgs(accountInfo)); + }; + + return ( + + + + + } + middleContent={ + + + + {accountInfo.name} + + + + + {accountInfo.description || "N/A"} + + + + } + /> + ); +}; diff --git a/src/lib/components/card/BlockCard.tsx b/src/lib/components/card/BlockCard.tsx new file mode 100644 index 000000000..522114104 --- /dev/null +++ b/src/lib/components/card/BlockCard.tsx @@ -0,0 +1,75 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "../ExplorerLink"; +import { ValidatorBadge } from "../ValidatorBadge"; +import { useInternalNavigate } from "lib/app-provider"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import type { BlockInfo } from "lib/types/block"; +import { dateFromNow, formatUTC, truncate } from "lib/utils"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +interface BlockCardProps { + blockData: BlockInfo; +} +export const BlockCard = ({ blockData }: BlockCardProps) => { + const navigate = useInternalNavigate(); + return ( + + navigate({ + pathname: "/blocks/[blockHeight]", + query: { blockHeight: blockData.height }, + }) + } + topContent={ + + + + + {blockData.height} + + + + + + + {truncate(blockData.hash.toUpperCase())} + + + + + + + + {blockData.txCount} + + + + + } + middleContent={ + + + + } + bottomContent={ + + + {formatUTC(blockData.timestamp)} + + {`(${dateFromNow(blockData.timestamp)})`} + + + + } + /> + ); +}; diff --git a/src/lib/components/card/ContractCard.tsx b/src/lib/components/card/ContractCard.tsx new file mode 100644 index 000000000..21ea07baf --- /dev/null +++ b/src/lib/components/card/ContractCard.tsx @@ -0,0 +1,87 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "../ExplorerLink"; +import { ContractNameCell } from "../table"; +import { InstantiatorRender } from "../table/contracts/ContractsTableRow"; +import { useInternalNavigate } from "lib/app-provider"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import type { ContractHistoryRemark, ContractInfo, Option } from "lib/types"; +import { RemarkOperation } from "lib/types"; +import { dateFromNow, formatUTC } from "lib/utils"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +interface InstantiatedContractCardProps { + contractInfo: ContractInfo; +} +const instantiatorRemark = (remark: Option) => { + if (!remark) { + return ; + } + if ( + remark.operation === + RemarkOperation.CONTRACT_CODE_HISTORY_OPERATION_TYPE_GENESIS + ) + return ; + if ( + remark.operation === + RemarkOperation.CONTRACT_CODE_HISTORY_OPERATION_TYPE_MIGRATE + ) + return ; + + return ; +}; +export const InstantiatedContractCard = ({ + contractInfo, +}: InstantiatedContractCardProps) => { + const navigate = useInternalNavigate(); + return ( + + navigate({ + pathname: "/contracts/[contractAddr]", + query: { contractAddr: contractInfo.contractAddress }, + }) + } + topContent={ + + + + + } + middleContent={ + + + + + + + {instantiatorRemark(contractInfo.remark)} + + + + } + bottomContent={ + <> + {contractInfo.latestUpdated && ( + + + {formatUTC(contractInfo.latestUpdated)} + + + {`(${dateFromNow(contractInfo.latestUpdated)})`} + + + )} + + } + /> + ); +}; diff --git a/src/lib/components/card/DefaultMobileCard.tsx b/src/lib/components/card/DefaultMobileCard.tsx new file mode 100644 index 000000000..c4cdb3bf7 --- /dev/null +++ b/src/lib/components/card/DefaultMobileCard.tsx @@ -0,0 +1,43 @@ +import { Flex } from "@chakra-ui/react"; +import type { ReactNode } from "react"; + +interface CardProps { + topContent: ReactNode; + middleContent: ReactNode; + bottomContent?: ReactNode; + onClick?: () => void; +} +export const DefaultMobileCard = ({ + topContent, + middleContent, + bottomContent, + onClick, +}: CardProps) => { + return ( + + + {topContent} + + + {middleContent} + + {bottomContent && {bottomContent}} + + ); +}; diff --git a/src/lib/components/card/MigrationCard.tsx b/src/lib/components/card/MigrationCard.tsx new file mode 100644 index 000000000..2833d5e27 --- /dev/null +++ b/src/lib/components/card/MigrationCard.tsx @@ -0,0 +1,99 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "../ExplorerLink"; +import { CodeNameCell } from "../table"; +import { useGetAddressType, useInternalNavigate } from "lib/app-provider"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import { RemarkRender } from "lib/pages/contract-details/components/tables/migration/MigrationRow"; +import type { ContractMigrationHistory } from "lib/types"; +import { dateFromNow, formatUTC, getCw2Info } from "lib/utils"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +interface MigrationCardProps { + history: ContractMigrationHistory; +} +export const MigrationCard = ({ history }: MigrationCardProps) => { + const getAddressType = useGetAddressType(); + const cw2Info = getCw2Info(history.cw2Contract, history.cw2Version); + const navigate = useInternalNavigate(); + return ( + + navigate({ + pathname: "/codes/[codeId]", + query: { codeId: history.codeId.toString() }, + }) + } + topContent={ + + + + + + + } + middleContent={ + + + + + + + + + {cw2Info ?? "N/A"} + + + + + + + + } + bottomContent={ + + + + + + + + + + + + + {formatUTC(history.timestamp)} + + ({dateFromNow(history.timestamp)}) + + + + } + /> + ); +}; diff --git a/src/lib/components/card/ProposalCard.tsx b/src/lib/components/card/ProposalCard.tsx new file mode 100644 index 000000000..6d7074e39 --- /dev/null +++ b/src/lib/components/card/ProposalCard.tsx @@ -0,0 +1,77 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "../ExplorerLink"; +import { Proposer } from "../table/proposals/Proposer"; +import { ResolvedHeight } from "../table/proposals/ResolvedHeight"; +import { StatusChip } from "../table/proposals/StatusChip"; +import { VotingEndTime } from "../table/proposals/VotingEndTime"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import type { Proposal } from "lib/types"; +import { ProposalStatus } from "lib/types"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +export interface ProposalCardProps { + proposal: Proposal; +} + +export const ProposalCard = ({ proposal }: ProposalCardProps) => { + const isDepositFailed = proposal.status === ProposalStatus.DEPOSIT_FAILED; + const isDepositOrVoting = + proposal.status === ProposalStatus.DEPOSIT_PERIOD || + proposal.status === ProposalStatus.VOTING_PERIOD; + return ( + + + + + + + + } + middleContent={ + + + + + {proposal.title} + + + Type: {proposal.type} + + + + + + + + } + bottomContent={ + <> + + + + + + + + + + } + /> + ); +}; diff --git a/src/lib/components/card/PublicContractCard.tsx b/src/lib/components/card/PublicContractCard.tsx new file mode 100644 index 000000000..3dba99bc3 --- /dev/null +++ b/src/lib/components/card/PublicContractCard.tsx @@ -0,0 +1,80 @@ +import { Button, Flex, Text } from "@chakra-ui/react"; + +import { AppLink } from "../AppLink"; +import { ExplorerLink } from "../ExplorerLink"; +import { ContractNameCell } from "../table"; +import { + useGetAddressTypeByLength, + useInternalNavigate, +} from "lib/app-provider"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import type { PublicContract } from "lib/types"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +interface PublicContractCardProps { + publicInfo: PublicContract; +} + +export const PublicContractCard = ({ publicInfo }: PublicContractCardProps) => { + const navigate = useInternalNavigate(); + const getAddressTypeByLength = useGetAddressTypeByLength(); + + return ( + + navigate({ + pathname: "/contracts/[contractAddr]", + query: { contractAddr: publicInfo.contractAddress }, + }) + } + topContent={ + <> + + + + + e.stopPropagation()} + > + + + + + + } + middleContent={ + + + + + + + {publicInfo.description} + + + + + + + + + + + } + /> + ); +}; diff --git a/src/lib/components/card/StoredCodeCard.tsx b/src/lib/components/card/StoredCodeCard.tsx new file mode 100644 index 000000000..af002173d --- /dev/null +++ b/src/lib/components/card/StoredCodeCard.tsx @@ -0,0 +1,80 @@ +import { Flex, Text } from "@chakra-ui/react"; + +import { ExplorerLink } from "../ExplorerLink"; +import { PermissionChip } from "../PermissionChip"; +import { CodeNameCell } from "../table"; +import { useInternalNavigate } from "lib/app-provider"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import type { CodeInfo, PublicCode } from "lib/types"; +import { getCw2Info } from "lib/utils"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +interface StoredCodeCardProps { + codeInfo: CodeInfo | PublicCode; +} +export const StoredCodeCard = ({ codeInfo }: StoredCodeCardProps) => { + const cw2Info = getCw2Info(codeInfo.cw2Contract, codeInfo.cw2Version); + const navigate = useInternalNavigate(); + return ( + + navigate({ + pathname: "/codes/[codeId]", + query: { codeId: codeInfo.id.toString() }, + }) + } + topContent={ + + + + + } + middleContent={ + + + + + + + + e.stopPropagation()} + > + {cw2Info ?? "N/A"} + + + + } + bottomContent={ + + + + e.stopPropagation()} + cursor="text" + color={codeInfo.contractCount ? "text.main" : "text.disabled"} + > + {codeInfo.contractCount ?? "N/A"} + + + + + + + + } + /> + ); +}; diff --git a/src/lib/components/card/TransactionCard.tsx b/src/lib/components/card/TransactionCard.tsx new file mode 100644 index 000000000..2c358f2ce --- /dev/null +++ b/src/lib/components/card/TransactionCard.tsx @@ -0,0 +1,82 @@ +import { Flex, Tag, Text } from "@chakra-ui/react"; + +import { RenderActionMessages } from "../action-msg/ActionMessages"; +import { ExplorerLink } from "../ExplorerLink"; +import { CustomIcon } from "../icon"; +import { RelationChip } from "../table/transactions/RelationChip"; +import { useInternalNavigate } from "lib/app-provider"; +import { MobileLabel } from "lib/pages/account-details/components/mobile/MobileLabel"; +import type { Transaction } from "lib/types"; +import { dateFromNow, formatUTC } from "lib/utils"; + +import { DefaultMobileCard } from "./DefaultMobileCard"; + +interface TransactionCardProps { + transaction: Transaction; + showRelations?: boolean; + showTimestamp?: boolean; +} +export const TransactionCard = ({ + transaction, + showRelations = true, + showTimestamp = true, +}: TransactionCardProps) => { + const navigate = useInternalNavigate(); + return ( + + navigate({ + pathname: "/txs/[txHash]", + query: { txHash: transaction.hash.toLocaleUpperCase() }, + }) + } + topContent={ + <> + + {transaction.success ? ( + + ) : ( + + )} + + + {showRelations && } + + } + middleContent={ + + + {transaction.isIbc && ( + + IBC + + )} + + } + bottomContent={ + + + + + + {showTimestamp && ( + + {formatUTC(transaction.created)} + + {`(${dateFromNow(transaction.created)})`} + + + )} + + } + /> + ); +}; diff --git a/src/lib/components/copy/Copier.tsx b/src/lib/components/copy/Copier.tsx index 773e32d50..74000ad8e 100644 --- a/src/lib/components/copy/Copier.tsx +++ b/src/lib/components/copy/Copier.tsx @@ -1,4 +1,4 @@ -import type { LayoutProps } from "@chakra-ui/react"; +import type { IconProps, LayoutProps } from "@chakra-ui/react"; import { CustomIcon } from "../icon"; import { AmpTrackCopier } from "lib/services/amplitude"; @@ -10,7 +10,7 @@ interface CopierProps { value: string; copyLabel?: string; display?: LayoutProps["display"]; - ml?: string; + ml?: IconProps["ml"]; amptrackSection?: string; } @@ -18,8 +18,8 @@ export const Copier = ({ type, value, copyLabel, - display = "block", - ml = "8px", + display = "inline", + ml = 2, amptrackSection, }: CopierProps) => ( AmpTrackCopier(amptrackSection, type)} name="copy" - boxSize="12px" - color="pebble.600" + boxSize={3} + color="gray.600" + minH={{ base: 6, md: "auto" }} /> } /> diff --git a/src/lib/components/copy/CopyButton.tsx b/src/lib/components/copy/CopyButton.tsx index 9de15a466..944a80434 100644 --- a/src/lib/components/copy/CopyButton.tsx +++ b/src/lib/components/copy/CopyButton.tsx @@ -21,7 +21,7 @@ export const CopyButton = ({ size = "sm", copyLabel, hasIcon = true, - variant = "outline-info", + variant = "outline-accent", buttonText = "Copy", amptrackSection, ...buttonProps @@ -39,7 +39,7 @@ export const CopyButton = ({ onClick={() => AmpTrack(AmpEvent.USE_COPY_BUTTON, { section: amptrackSection }) } - leftIcon={hasIcon ? : undefined} + leftIcon={hasIcon ? : undefined} {...buttonProps} > {buttonText} diff --git a/src/lib/components/copy/CopyTemplate.tsx b/src/lib/components/copy/CopyTemplate.tsx index 8eb7ae387..362ed2abd 100644 --- a/src/lib/components/copy/CopyTemplate.tsx +++ b/src/lib/components/copy/CopyTemplate.tsx @@ -22,6 +22,7 @@ export const CopyTemplate = ({ return ( { onCopy(); e.stopPropagation(); diff --git a/src/lib/components/dropzone/index.tsx b/src/lib/components/dropzone/index.tsx index 793b1870f..4559e4d8d 100644 --- a/src/lib/components/dropzone/index.tsx +++ b/src/lib/components/dropzone/index.tsx @@ -4,7 +4,7 @@ import { useCallback } from "react"; import { useDropzone } from "react-dropzone"; import { UploadIcon } from "../icon"; -import { MAX_FILE_SIZE } from "lib/data"; +import { useWasmConfig } from "lib/app-provider"; interface DropZoneProps extends DetailedHTMLProps, HTMLDivElement> { @@ -12,6 +12,8 @@ interface DropZoneProps } export function DropZone({ setFile, ...componentProps }: DropZoneProps) { + const wasm = useWasmConfig({ shouldRedirect: false }); + const onDrop = useCallback( (file: File[]) => { setFile(file[0]); @@ -19,47 +21,57 @@ export function DropZone({ setFile, ...componentProps }: DropZoneProps) { [setFile] ); - const { getRootProps, getInputProps } = useDropzone({ + // Throwing error when wasm is disabled will cause the page to not redirect, so default value is assigned instead + const maxSize = wasm.enabled ? wasm.storeCodeMaxFileSize : 0; + + const { getRootProps, getInputProps, fileRejections } = useDropzone({ onDrop, maxFiles: 1, accept: { "application/wasm": [".wasm"], }, - maxSize: MAX_FILE_SIZE, + maxSize, }); return ( - - - - - - Click to upload + + 0 ? "error.main" : "gray.700"} + w="full" + p="24px 16px" + borderRadius="8px" + align="center" + direction="column" + _hover={{ bg: "gray.900" }} + transition="all .25s ease-in-out" + cursor="pointer" + {...getRootProps()} + {...componentProps} + > + + + + + Click to upload + + or drag Wasm file here + + + .wasm (max. {maxSize / 1000}KB) - or drag Wasm file here - - .wasm (max. 800KB) - + {fileRejections.length > 0 && ( + + {fileRejections[0].errors[0].message} + + )} ); } diff --git a/src/lib/components/filter/FilterComponents.ts b/src/lib/components/filter/FilterComponents.ts index 8c3cf5d7d..e50dec739 100644 --- a/src/lib/components/filter/FilterComponents.ts +++ b/src/lib/components/filter/FilterComponents.ts @@ -3,7 +3,7 @@ import { chakra, List } from "@chakra-ui/react"; export const DropdownContainer = chakra(List, { baseStyle: { borderRadius: "8px", - bg: "pebble.900", + bg: "gray.900", px: 2, py: 1, mt: 0, diff --git a/src/lib/components/filter/FilterDropdownItem.tsx b/src/lib/components/filter/FilterDropdownItem.tsx index 977a61e17..40a239d64 100644 --- a/src/lib/components/filter/FilterDropdownItem.tsx +++ b/src/lib/components/filter/FilterDropdownItem.tsx @@ -23,13 +23,13 @@ export const FilterDropdownItem = ({ }: FilterDropdownItemProps) => ( {filterDropdownComponent} - {isOptionSelected && } + {isOptionSelected && } ); diff --git a/src/lib/components/filter/FilterInput.tsx b/src/lib/components/filter/FilterInput.tsx index dcbd6a29d..c80f22093 100644 --- a/src/lib/components/filter/FilterInput.tsx +++ b/src/lib/components/filter/FilterInput.tsx @@ -36,7 +36,7 @@ export const FilterInput = ({ background="none" borderRadius="8px" border="1px solid" - borderColor="pebble.700" + borderColor="gray.700" overflowX="scroll" alignItems="center" > @@ -57,12 +57,12 @@ export const FilterInput = ({ setIsDropdown(!isDropdown)} /> @@ -72,7 +72,7 @@ export const FilterInput = ({ position="absolute" top={0} left={0} - fontWeight="400" + fontWeight={400} color="text.dark" bgColor="background.main" pointerEvents="none" diff --git a/src/lib/components/forms/AssetInput.tsx b/src/lib/components/forms/AssetInput.tsx index 6c003556e..a3605f2af 100644 --- a/src/lib/components/forms/AssetInput.tsx +++ b/src/lib/components/forms/AssetInput.tsx @@ -27,7 +27,7 @@ export const AssetInput = ({ assetOptions, initialSelected, }: AssetInputProps) => ( - + ({ const isError = !!error; const isRequired = "required" in rules; + return ( ({ }); const { field } = useController({ name, control, rules }); const isError = Boolean(error); + const isRequired = "required" in rules; return ( div": { marginTop: "1 !important" } }} {...componentProps} {...field} @@ -56,10 +58,10 @@ export const ControllerTextarea = ({ )}