From 0343295ab306e215e25897be011f3dba4b8cc455 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:12:11 +0200 Subject: [PATCH 01/21] Remove old context provider --- context/AppContext.tsx | 82 ------------------------------------------ context/AppReducer.ts | 33 ----------------- 2 files changed, 115 deletions(-) delete mode 100644 context/AppContext.tsx delete mode 100644 context/AppReducer.ts diff --git a/context/AppContext.tsx b/context/AppContext.tsx deleted file mode 100644 index 3b3ac98..0000000 --- a/context/AppContext.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { createContext, useContext, useEffect, useReducer } from "react"; -import { ChainInfo } from "../types"; -import { AppReducer, ChangeChainAction, initialState } from "./AppReducer"; - -export interface AppContextType { - chain: ChainInfo; -} - -const AppContext = createContext<{ - state: AppContextType; - dispatch: React.Dispatch; -}>({ state: initialState, dispatch: () => {} }); - -function getChainInfoFromUrl(): ChainInfo { - const url = location.search; - const params = new URLSearchParams(url); - const chainInfo: ChainInfo = { - nodeAddress: decodeURIComponent(params.get("nodeAddress") || ""), - denom: decodeURIComponent(params.get("denom") || ""), - displayDenom: decodeURIComponent(params.get("displayDenom") || ""), - displayDenomExponent: parseInt( - decodeURIComponent(params.get("displayDenomExponent") || ""), - 10, - ), - assets: JSON.parse(decodeURIComponent(params.get("assets") || "[]")), - gasPrice: decodeURIComponent(params.get("gasPrice") || ""), - chainId: decodeURIComponent(params.get("chainId") || ""), - chainDisplayName: decodeURIComponent(params.get("chainDisplayName") || ""), - registryName: decodeURIComponent(params.get("registryName") || ""), - addressPrefix: decodeURIComponent(params.get("addressPrefix") || ""), - explorerLink: decodeURIComponent(params.get("explorerLink") || ""), - }; - - return chainInfo; -} - -function setChainInfoParams(chainInfo: ChainInfo) { - const params = new URLSearchParams(); - - for (const [key, value] of Object.entries(chainInfo)) { - if (Array.isArray(value)) { - params.set(key, encodeURIComponent(JSON.stringify(value))); - } else { - params.set(key, encodeURIComponent(value ?? "")); - } - } - - window.history.replaceState({}, "", `${location.pathname}?${params}`); -} - -export function AppWrapper({ children }: { children: React.ReactNode }) { - let existingState; - if (typeof window !== "undefined") { - const storedState = localStorage.getItem("state"); - if (storedState) { - existingState = JSON.parse(storedState); - } - - const urlChainInfo = getChainInfoFromUrl(); - - // query params should override saved state - if (urlChainInfo.chainId) { - console.log("setting state from url"); - existingState = { chain: urlChainInfo }; - } - } - const [state, dispatch] = useReducer(AppReducer, existingState ? existingState : initialState); - - const contextValue = { state, dispatch }; - - useEffect(() => { - if (state && state !== initialState) { - localStorage.setItem("state", JSON.stringify(state)); - setChainInfoParams(state.chain); - } - }, [state]); - - return {children}; -} -export function useAppContext() { - return useContext(AppContext); -} diff --git a/context/AppReducer.ts b/context/AppReducer.ts deleted file mode 100644 index 0a4f124..0000000 --- a/context/AppReducer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ChainInfo } from "../types"; -import { AppContextType } from "./AppContext"; - -export const initialState: AppContextType = { - chain: { - nodeAddress: process.env.NEXT_PUBLIC_NODE_ADDRESS, - denom: process.env.NEXT_PUBLIC_DENOM, - displayDenom: process.env.NEXT_PUBLIC_DISPLAY_DENOM, - displayDenomExponent: parseInt(process.env.NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT || "", 10), - gasPrice: process.env.NEXT_PUBLIC_GAS_PRICE, - chainId: process.env.NEXT_PUBLIC_CHAIN_ID, - chainDisplayName: process.env.NEXT_PUBLIC_CHAIN_DISPLAY_NAME, - registryName: process.env.NEXT_PUBLIC_REGISTRY_NAME, - addressPrefix: process.env.NEXT_PUBLIC_ADDRESS_PREFIX, - explorerLink: process.env.NEXT_PUBLIC_EXPLORER_LINK_TX, - }, -}; - -export interface ChangeChainAction { - type: "changeChain"; - value: ChainInfo; -} - -export const AppReducer = (state: AppContextType, action: ChangeChainAction) => { - switch (action.type) { - case "changeChain": { - return { - ...state, - chain: action.value, - }; - } - } -}; From 599d0844dff06dfd03adcc9ffcce76e9f2c0dfbe Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:16:51 +0200 Subject: [PATCH 02/21] Move types around and co-locate context types --- context/ChainsContext/types.tsx | 47 ++++++++++++ lib/displayHelpers.ts | 2 +- .../chainRegistry.ts | 72 ++++++++----------- types/index.ts | 15 ---- 4 files changed, 77 insertions(+), 59 deletions(-) create mode 100644 context/ChainsContext/types.tsx rename components/chainSelect/chainregistry.ts => types/chainRegistry.ts (53%) diff --git a/context/ChainsContext/types.tsx b/context/ChainsContext/types.tsx new file mode 100644 index 0000000..6ceda93 --- /dev/null +++ b/context/ChainsContext/types.tsx @@ -0,0 +1,47 @@ +import { GithubChainRegistryItem, RegistryAsset } from "../../types/chainRegistry"; + +export interface ChainsContextType { + readonly state: State; + readonly dispatch: Dispatch; +} + +export interface State { + readonly chains: ChainItems; + readonly chain: ChainInfo; + readonly chainsError?: string | null; +} + +export type Dispatch = (action: Action) => void; + +export interface ChainItems { + readonly mainnets: readonly GithubChainRegistryItem[]; + readonly testnets: readonly GithubChainRegistryItem[]; +} + +export interface ChainInfo { + readonly registryName: string; + readonly chainId: string; + readonly chainDisplayName: string; + readonly nodeAddress: string; + readonly denom: string; + readonly displayDenom: string; + readonly displayDenomExponent: number; + readonly assets: readonly RegistryAsset[]; + readonly gasPrice: string; + readonly addressPrefix: string; + readonly explorerLink: string; +} + +export type Action = + | { + readonly type: "setChains"; + readonly payload: ChainItems; + } + | { + readonly type: "setChain"; + readonly payload: ChainInfo; + } + | { + readonly type: "setChainsError"; + readonly payload: string | null; + }; diff --git a/lib/displayHelpers.ts b/lib/displayHelpers.ts index 100bf61..095c274 100644 --- a/lib/displayHelpers.ts +++ b/lib/displayHelpers.ts @@ -2,7 +2,7 @@ import { Coin } from "@cosmjs/amino"; import { sha512 } from "@cosmjs/crypto"; import { fromBase64, fromBech32, toBase64, toBech32 } from "@cosmjs/encoding"; import { Decimal } from "@cosmjs/math"; -import { ChainInfo } from "../types"; +import { ChainInfo } from "../context/ChainsContext/types"; function capitalizeFirstLetter(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); diff --git a/components/chainSelect/chainregistry.ts b/types/chainRegistry.ts similarity index 53% rename from components/chainSelect/chainregistry.ts rename to types/chainRegistry.ts index a8f068d..f95acb4 100644 --- a/components/chainSelect/chainregistry.ts +++ b/types/chainRegistry.ts @@ -1,4 +1,19 @@ -import axios from "axios"; +export interface GithubChainRegistryItem { + name: string; + path: string; + sha: string; + size: number; + url: string; + html_url: string; + git_url: string; + download_url: string | null; + type: string; + _links: { + self: string; + git: string; + html: string; + }; +} export interface RegistryChainApisRpc { readonly address: string; @@ -36,57 +51,28 @@ export interface RegistryChain { readonly pretty_name: string; } -export interface RegistryChainResponse { - readonly data: RegistryChain; -} - /** * See https://github.com/cosmos/chain-registry/blob/1e9ecde770951cab90f0853a624411d79af90b83/provenance/assetlist.json#L8-L12 */ export interface RegistryAssetDenomUnit { - denom: string; - exponent: number; - aliases?: string[]; + readonly denom: string; + readonly exponent: number; + readonly aliases?: readonly string[]; } /** * See https://github.com/cosmos/chain-registry/blob/1e9ecde770951cab90f0853a624411d79af90b83/provenance/assetlist.json#L5-L28 */ export interface RegistryAsset { - description: string; - denom_units: RegistryAssetDenomUnit[]; - base: string; - name: string; - display: string; - symbol: string; - logo_URIs: { - png: string; - svg: string; + readonly denom_units: readonly RegistryAssetDenomUnit[]; + readonly base: string; + readonly display: string; + readonly name: string; + readonly symbol: string; + readonly description?: string; + readonly logo_URIs?: { + readonly png: string; + readonly svg: string; }; - coingecko_id: string; + readonly coingecko_id?: string; } - -export interface RegistryAssetsResponse { - readonly data: { readonly assets: readonly RegistryAsset[] }; -} - -const registryGhUrl = "https://cdn.jsdelivr.net/gh/cosmos/chain-registry@master/"; - -export const getChainFromRegistry = async (chainGhName: string): Promise => { - const chainGhUrl = registryGhUrl + chainGhName + "/chain.json"; - - const { data: chain }: RegistryChainResponse = await axios.get(chainGhUrl); - return chain; -}; - -export const getAssetsFromRegistry = async ( - chainGhName: string, -): Promise => { - const assetsGhUrl = registryGhUrl + chainGhName + "/assetlist.json"; - - const { - data: { assets }, - }: RegistryAssetsResponse = await axios.get(assetsGhUrl); - - return assets; -}; diff --git a/types/index.ts b/types/index.ts index b01fb4d..e7aaa2b 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,6 +1,5 @@ import { StdFee } from "@cosmjs/amino"; import { EncodeObject } from "@cosmjs/proto-signing"; -import { RegistryAsset } from "../components/chainSelect/chainregistry"; declare global { interface Window { @@ -46,17 +45,3 @@ export interface WalletAccount { isNanoLedger?: boolean; name?: string; } - -export interface ChainInfo { - nodeAddress?: string; - denom?: string; - displayDenom?: string; - displayDenomExponent?: number; - assets?: readonly RegistryAsset[]; - gasPrice?: string; - chainId?: string; - chainDisplayName?: string; - registryName?: string; - addressPrefix?: string; - explorerLink?: string; -} From 11eab4d0691737fd22acf1719341aba124cbc6bb Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:17:29 +0200 Subject: [PATCH 03/21] Add ChainsContext helpers --- context/ChainsContext/helpers.tsx | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 context/ChainsContext/helpers.tsx diff --git a/context/ChainsContext/helpers.tsx b/context/ChainsContext/helpers.tsx new file mode 100644 index 0000000..3271c3b --- /dev/null +++ b/context/ChainsContext/helpers.tsx @@ -0,0 +1,36 @@ +import { ChainInfo, ChainItems, Dispatch } from "./types"; + +export const emptyChain: ChainInfo = { + nodeAddress: "", + denom: "", + displayDenom: "", + displayDenomExponent: 0, + assets: [], + gasPrice: "", + chainId: "", + chainDisplayName: "", + registryName: "", + addressPrefix: "", + explorerLink: "", +}; + +export const isChainInfoFilled = ({ displayDenomExponent, assets, ...restFields }: ChainInfo) => + displayDenomExponent >= 0 && + assets.length > 0 && + Object.values(restFields).every((value) => value !== ""); + +export const setChains = (dispatch: Dispatch, chains: ChainItems) => { + dispatch({ type: "setChains", payload: chains }); +}; + +export const setChain = (dispatch: Dispatch, chain: ChainInfo) => { + dispatch({ type: "setChain", payload: chain }); +}; + +export const setChainFromRegistry = (dispatch: Dispatch, chainName: string) => { + dispatch({ type: "setChain", payload: { ...emptyChain, registryName: chainName } }); +}; + +export const setChainsError = (dispatch: Dispatch, chainsError: string | null) => { + dispatch({ type: "setChainsError", payload: chainsError }); +}; From 6316f24796d3459ac9ade674372bee874fc7c42a Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:17:57 +0200 Subject: [PATCH 04/21] Add ChainsContext storage functions --- context/ChainsContext/storage.tsx | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 context/ChainsContext/storage.tsx diff --git a/context/ChainsContext/storage.tsx b/context/ChainsContext/storage.tsx new file mode 100644 index 0000000..7ed10ae --- /dev/null +++ b/context/ChainsContext/storage.tsx @@ -0,0 +1,76 @@ +import { isChainInfoFilled } from "./helpers"; +import { ChainInfo } from "./types"; + +const localStorageKey = "context-chain-info"; + +export const getChainFromUrl = (chainName: string | undefined) => { + const params = new URLSearchParams(location.search); + + const chain: ChainInfo = { + registryName: chainName || "", + chainId: decodeURIComponent(params.get("chainId") || ""), + nodeAddress: decodeURIComponent(params.get("nodeAddress") || ""), + denom: decodeURIComponent(params.get("denom") || ""), + displayDenom: decodeURIComponent(params.get("displayDenom") || ""), + displayDenomExponent: Number(decodeURIComponent(params.get("displayDenomExponent") || "")), + assets: JSON.parse(decodeURIComponent(params.get("assets") || "[]")), + gasPrice: decodeURIComponent(params.get("gasPrice") || ""), + chainDisplayName: decodeURIComponent(params.get("chainDisplayName") || ""), + addressPrefix: decodeURIComponent(params.get("addressPrefix") || ""), + explorerLink: decodeURIComponent(params.get("explorerLink") || ""), + }; + + return isChainInfoFilled(chain) ? chain : null; +}; + +const setChainInUrl = ({ registryName, ...restChainFields }: ChainInfo) => { + const params = new URLSearchParams(); + + for (const [key, value] of Object.entries(restChainFields)) { + if (typeof value === "object") { + params.set(key, encodeURIComponent(JSON.stringify(value))); + } else { + params.set(key, encodeURIComponent(value ?? "")); + } + } + + window.history.replaceState({}, registryName, `${location.pathname}?${params}`); +}; + +export const getChainFromStorage = (chainName: string | undefined) => { + const storedChain = localStorage.getItem(localStorageKey); + if (!storedChain) return null; + + const chain: ChainInfo = JSON.parse(storedChain); + const isChainNameValid = chain.registryName === chainName || !chainName; + return isChainNameValid && isChainInfoFilled(chain) ? chain : null; +}; + +const setChainInStorage = (chain: ChainInfo) => { + const stringChain = JSON.stringify(chain); + localStorage.setItem(localStorageKey, stringChain); +}; + +export const storeChain = (chain: ChainInfo) => { + setChainInStorage(chain); + setChainInUrl(chain); +}; + +export const getChainFromEnvfile = (chainName: string | undefined) => { + const chain: ChainInfo = { + nodeAddress: process.env.NEXT_PUBLIC_NODE_ADDRESS || "", + denom: process.env.NEXT_PUBLIC_DENOM || "", + displayDenom: process.env.NEXT_PUBLIC_DISPLAY_DENOM || "", + displayDenomExponent: Number(process.env.NEXT_PUBLIC_DISPLAY_DENOM_EXPONENT || 0), + assets: JSON.parse(process.env.NEXT_PUBLIC_ASSETS || "[]"), + gasPrice: process.env.NEXT_PUBLIC_GAS_PRICE || "", + chainId: process.env.NEXT_PUBLIC_CHAIN_ID || "", + chainDisplayName: process.env.NEXT_PUBLIC_CHAIN_DISPLAY_NAME || "", + registryName: process.env.NEXT_PUBLIC_REGISTRY_NAME || "", + addressPrefix: process.env.NEXT_PUBLIC_ADDRESS_PREFIX || "", + explorerLink: process.env.NEXT_PUBLIC_EXPLORER_LINK_TX || "", + }; + + const isChainNameValid = chain.registryName === chainName || !chainName; + return isChainNameValid && isChainInfoFilled(chain) ? chain : null; +}; From 57246c88f422ca6005b8efe86bdecf3fcb0f8cf9 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:18:21 +0200 Subject: [PATCH 05/21] Add ChainsContext service functions --- context/ChainsContext/service.tsx | 149 ++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 context/ChainsContext/service.tsx diff --git a/context/ChainsContext/service.tsx b/context/ChainsContext/service.tsx new file mode 100644 index 0000000..3233919 --- /dev/null +++ b/context/ChainsContext/service.tsx @@ -0,0 +1,149 @@ +import { StargateClient } from "@cosmjs/stargate"; +import { + GithubChainRegistryItem, + RegistryAsset, + RegistryChain, + RegistryChainApisRpc, + RegistryChainExplorer, +} from "../../types/chainRegistry"; +import { emptyChain, isChainInfoFilled } from "./helpers"; +import { getChainFromEnvfile, getChainFromStorage, getChainFromUrl } from "./storage"; +import { ChainInfo, ChainItems } from "./types"; + +const chainsUrl = "https://api.github.com/repos/cosmos/chain-registry/contents"; +const testnetsUrl = "https://api.github.com/repos/cosmos/chain-registry/contents/testnets"; +const registryGhUrl = "https://cdn.jsdelivr.net/gh/cosmos/chain-registry@master/"; + +const getChains = async (chainUrl: string) => { + const response = await fetch(chainUrl); + if (!response.ok) { + throw new Error("Failed to get chains from registry"); + } + + const chainItems: readonly GithubChainRegistryItem[] = await response.json(); + return chainItems; +}; + +export const getChainItemsFromRegistry: () => Promise = async () => { + const [mainnets, testnets] = await Promise.all([getChains(chainsUrl), getChains(testnetsUrl)]); + + const nonChainsFilter = (item: GithubChainRegistryItem) => + item.type === "dir" && !item.name.startsWith(".") && !item.name.startsWith("_"); + + return { + mainnets: mainnets.filter(nonChainsFilter), + testnets: testnets.filter(nonChainsFilter), + }; +}; + +export const getChainItemFromRegistry = async (chainName: string, isTestnet?: boolean) => { + const chainGhPath = isTestnet ? "testnets/" + chainName : chainName; + const chainGhUrl = registryGhUrl + chainGhPath + "/chain.json"; + + const response = await fetch(chainGhUrl); + if (!response.ok) { + throw new Error(`Failed to get ${chainName} chain from registry`); + } + + const chain: RegistryChain = await response.json(); + return chain; +}; + +export const getAssetItemsFromRegistry = async (chainName: string, isTestnet?: boolean) => { + const assetsGhPath = isTestnet ? "testnets/" + chainName : chainName; + const assetsGhUrl = registryGhUrl + assetsGhPath + "/assetlist.json"; + + const response = await fetch(assetsGhUrl); + if (!response.ok) { + throw new Error(`Failed to get assets for ${chainName} chain from registry`); + } + + const assets: readonly RegistryAsset[] = (await response.json()).assets; + return assets; +}; + +const getNodeFromArray = async (nodeArray: readonly RegistryChainApisRpc[]) => { + // only return https connections + const secureNodes = nodeArray + .filter(({ address }) => address.startsWith("https://")) + .map(({ address }) => address); + + if (!secureNodes.length) { + throw new Error("No SSL enabled RPC nodes available for this chain"); + } + + for (const node of secureNodes) { + try { + // test client connection + const client = await StargateClient.connect(node); + await client.getHeight(); + return node; + } catch {} + } + + throw new Error("No RPC nodes available for this chain"); +}; + +const getExplorerFromArray = (explorers: readonly RegistryChainExplorer[]) => { + return explorers[0]?.tx_page ?? ""; +}; + +export const getChainFromRegistry = async (chainName: string, isTestnet?: boolean) => { + const chainItem = await getChainItemFromRegistry(chainName, isTestnet); + const registryAssets = await getAssetItemsFromRegistry(chainName, isTestnet); + const firstAsset = registryAssets[0]; + + const nodeAddress = await getNodeFromArray(chainItem.apis.rpc); + const explorerLink = getExplorerFromArray(chainItem.explorers); + const firstAssetDenom = firstAsset.base; + const displayDenom = firstAsset.symbol; + const displayUnit = firstAsset.denom_units.find((u) => u.denom == firstAsset.display); + const displayDenomExponent = displayUnit?.exponent ?? 6; // TODO remove hardcoded fallback 6 + + const feeToken = chainItem.fees.fee_tokens.find((token) => token.denom == firstAssetDenom) ?? { + denom: firstAssetDenom, + }; + const gasPrice = + feeToken.average_gas_price ?? + feeToken.low_gas_price ?? + feeToken.high_gas_price ?? + feeToken.fixed_min_gas_price ?? + 0.03; + const formattedGasPrice = firstAsset ? `${gasPrice}${firstAssetDenom}` : ""; + + const chain: ChainInfo = { + registryName: chainName, + addressPrefix: chainItem.bech32_prefix, + chainId: chainItem.chain_id, + chainDisplayName: chainItem.pretty_name, + nodeAddress, + explorerLink, + denom: firstAssetDenom, + displayDenom, + displayDenomExponent, + gasPrice: formattedGasPrice, + assets: registryAssets, + }; + + if (!isChainInfoFilled(chain)) { + throw new Error(`Chain ${chainName} loaded from the registry with missing data`); + } + + return chain; +}; + +export const getChain = () => { + if (typeof window === "undefined") return emptyChain; + + const chainName = location.pathname.split("/")[1]; + const chainFromUrl = getChainFromUrl(chainName); + if (chainFromUrl) return chainFromUrl; + + const chainFromStorage = getChainFromStorage(chainName); + if (chainFromStorage) return chainFromStorage; + + const chainFromEnvfile = getChainFromEnvfile(chainName); + if (chainFromEnvfile) return chainFromEnvfile; + + return emptyChain; +}; From a26efde7d52e08c397bfdf15600c5133ccad7920 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:19:27 +0200 Subject: [PATCH 06/21] Add ChainsProvider with useChains --- context/ChainsContext/index.tsx | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 context/ChainsContext/index.tsx diff --git a/context/ChainsContext/index.tsx b/context/ChainsContext/index.tsx new file mode 100644 index 0000000..3aab2ec --- /dev/null +++ b/context/ChainsContext/index.tsx @@ -0,0 +1,91 @@ +import { ReactNode, createContext, useContext, useEffect, useReducer } from "react"; +import { setChain, setChains, setChainsError } from "./helpers"; +import { getChain, getChainFromRegistry, getChainItemsFromRegistry } from "./service"; +import { storeChain } from "./storage"; +import { Action, ChainsContextType, State } from "./types"; + +const ChainsContext = createContext(undefined); + +const chainsReducer = (state: State, action: Action) => { + switch (action.type) { + case "setChains": { + return { ...state, chains: action.payload }; + } + case "setChain": { + storeChain(action.payload); + return { ...state, chain: action.payload }; + } + case "setChainsError": { + return { ...state, chainsError: action.payload }; + } + default: { + throw new Error("Unhandled action type"); + } + } +}; + +interface ChainsProviderProps { + readonly children: ReactNode; +} + +export const ChainsProvider = ({ children }: ChainsProviderProps) => { + const [state, dispatch] = useReducer(chainsReducer, { + chain: getChain(), + chains: { mainnets: [], testnets: [] }, + }); + + useEffect(() => { + (async function getChainsFromGithubRegistry() { + try { + const newChainItems = await getChainItemsFromRegistry(); + setChains(dispatch, newChainItems); + } catch (error) { + if (error instanceof Error) { + setChainsError(dispatch, error.message); + } else { + setChainsError(dispatch, "Failed to get chains from registry"); + } + } + })(); + }, []); + + useEffect(() => { + (async function getChainFromRegistryIfEmpty() { + if (!state.chain.chainId && state.chains.mainnets.length && state.chains.testnets.length) { + const chainName = + state.chain.registryName || + location.pathname.split("/")[1] || + process.env.NEXT_PUBLIC_REGISTRY_NAME || + "cosmoshub"; + + const isTestnet = !!state.chains.testnets.find(({ name }) => name === chainName); + + try { + const chainFromRegistry = await getChainFromRegistry(chainName, isTestnet); + setChain(dispatch, chainFromRegistry); + } catch (error) { + if (error instanceof Error) { + setChainsError(dispatch, error.message); + } else { + setChainsError(dispatch, "Failed to get chains from registry"); + } + } + } + })(); + }, [ + state.chain.chainId, + state.chain.registryName, + state.chains.mainnets.length, + state.chains.testnets, + ]); + + return {children}; +}; + +export const useChains = () => { + const context = useContext(ChainsContext); + if (context === undefined) { + throw new Error("useChains must be used within a ChainsProvider"); + } + return { ...context.state, chainsDispatch: context.dispatch }; +}; From 166db9a758a2dc1273b745d7e76d80afbfae5ab4 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:20:08 +0200 Subject: [PATCH 07/21] Use new hook in dataViews --- components/dataViews/CompletedTransaction.tsx | 12 ++++++------ components/dataViews/MultisigHoldings.tsx | 12 ++++++------ .../TxMsgCreateVestingAccountDetails.tsx | 6 +++--- .../TransactionInfo/TxMsgDelegateDetails.tsx | 6 +++--- .../TransactionInfo/TxMsgRedelegateDetails.tsx | 6 +++--- .../dataViews/TransactionInfo/TxMsgSendDetails.tsx | 6 +++--- .../TransactionInfo/TxMsgUndelegateDetails.tsx | 6 +++--- components/dataViews/TransactionInfo/index.tsx | 6 +++--- 8 files changed, 30 insertions(+), 30 deletions(-) diff --git a/components/dataViews/CompletedTransaction.tsx b/components/dataViews/CompletedTransaction.tsx index a8ca3c7..ec49e71 100644 --- a/components/dataViews/CompletedTransaction.tsx +++ b/components/dataViews/CompletedTransaction.tsx @@ -1,16 +1,16 @@ -import { useAppContext } from "../../context/AppContext"; +import { useChains } from "../../context/ChainsContext"; import { explorerLinkTx } from "../../lib/displayHelpers"; import Button from "../inputs/Button"; import StackableContainer from "../layout/StackableContainer"; import HashView from "./HashView"; -interface Props { - transactionHash: string; +interface CompletedTransactionProps { + readonly transactionHash: string; } -const CompletedTransaction = ({ transactionHash }: Props) => { - const { state } = useAppContext(); - const baseURL = state.chain.explorerLink ? state.chain.explorerLink : ""; +const CompletedTransaction = ({ transactionHash }: CompletedTransactionProps) => { + const { chain } = useChains(); + const baseURL = chain.explorerLink ? chain.explorerLink : ""; const explorerLink = explorerLinkTx(baseURL, transactionHash); return ( diff --git a/components/dataViews/MultisigHoldings.tsx b/components/dataViews/MultisigHoldings.tsx index 61e1bf5..454bca0 100644 --- a/components/dataViews/MultisigHoldings.tsx +++ b/components/dataViews/MultisigHoldings.tsx @@ -1,14 +1,14 @@ import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; -import { useAppContext } from "../../context/AppContext"; +import { useChains } from "../../context/ChainsContext"; import { printableCoin } from "../../lib/displayHelpers"; import StackableContainer from "../layout/StackableContainer"; -interface Props { - holdings: readonly Coin[]; +interface MultisigHoldingsProps { + readonly holdings: readonly Coin[]; } -const MultisigHoldings = (props: Props) => { - const { state } = useAppContext(); +const MultisigHoldings = (props: MultisigHoldingsProps) => { + const { chain } = useChains(); return (

Holdings

@@ -16,7 +16,7 @@ const MultisigHoldings = (props: Props) => { {props.holdings.length ? ( props.holdings.map((holding) => ( - {printableCoin(holding, state.chain)} + {printableCoin(holding, chain)} )) ) : ( diff --git a/components/dataViews/TransactionInfo/TxMsgCreateVestingAccountDetails.tsx b/components/dataViews/TransactionInfo/TxMsgCreateVestingAccountDetails.tsx index 3091831..6e702fd 100644 --- a/components/dataViews/TransactionInfo/TxMsgCreateVestingAccountDetails.tsx +++ b/components/dataViews/TransactionInfo/TxMsgCreateVestingAccountDetails.tsx @@ -1,5 +1,5 @@ import { MsgCreateVestingAccount } from "cosmjs-types/cosmos/vesting/v1beta1/tx"; -import { useAppContext } from "../../../context/AppContext"; +import { useChains } from "../../../context/ChainsContext"; import { printableCoins } from "../../../lib/displayHelpers"; import HashView from "../HashView"; @@ -8,7 +8,7 @@ interface TxMsgCreateVestingAccountDetailsProps { } const TxMsgCreateVestingAccountDetails = ({ msgValue }: TxMsgCreateVestingAccountDetailsProps) => { - const { state } = useAppContext(); + const { chain } = useChains(); const endTimeDateObj = new Date(msgValue.endTime.multiply(1000).toNumber()); const endTimeDate = endTimeDateObj.toLocaleDateString(); const endTimeHours = endTimeDateObj.toLocaleTimeString().slice(0, -3); @@ -20,7 +20,7 @@ const TxMsgCreateVestingAccountDetails = ({ msgValue }: TxMsgCreateVestingAccoun
  • -
    {printableCoins(msgValue.amount, state.chain)}
    +
    {printableCoins(msgValue.amount, chain)}
  • diff --git a/components/dataViews/TransactionInfo/TxMsgDelegateDetails.tsx b/components/dataViews/TransactionInfo/TxMsgDelegateDetails.tsx index 0b758a1..2099083 100644 --- a/components/dataViews/TransactionInfo/TxMsgDelegateDetails.tsx +++ b/components/dataViews/TransactionInfo/TxMsgDelegateDetails.tsx @@ -1,6 +1,6 @@ import { assert } from "@cosmjs/utils"; import { MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; -import { useAppContext } from "../../../context/AppContext"; +import { useChains } from "../../../context/ChainsContext"; import { printableCoin } from "../../../lib/displayHelpers"; import HashView from "../HashView"; @@ -9,7 +9,7 @@ interface TxMsgDelegateDetailsProps { } const TxMsgDelegateDetails = ({ msgValue }: TxMsgDelegateDetailsProps) => { - const { state } = useAppContext(); + const { chain } = useChains(); assert( msgValue.amount, "Amount must be set, see https://github.com/osmosis-labs/telescope/issues/386", @@ -22,7 +22,7 @@ const TxMsgDelegateDetails = ({ msgValue }: TxMsgDelegateDetailsProps) => {
  • -
    {printableCoin(msgValue.amount, state.chain)}
    +
    {printableCoin(msgValue.amount, chain)}
  • diff --git a/components/dataViews/TransactionInfo/TxMsgRedelegateDetails.tsx b/components/dataViews/TransactionInfo/TxMsgRedelegateDetails.tsx index 4e2a842..66a39db 100644 --- a/components/dataViews/TransactionInfo/TxMsgRedelegateDetails.tsx +++ b/components/dataViews/TransactionInfo/TxMsgRedelegateDetails.tsx @@ -1,6 +1,6 @@ import { assert } from "@cosmjs/utils"; import { MsgBeginRedelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; -import { useAppContext } from "../../../context/AppContext"; +import { useChains } from "../../../context/ChainsContext"; import { printableCoin } from "../../../lib/displayHelpers"; import HashView from "../HashView"; @@ -9,7 +9,7 @@ interface TxMsgRedelegateDetailsProps { } const TxMsgRedelegateDetails = ({ msgValue }: TxMsgRedelegateDetailsProps) => { - const { state } = useAppContext(); + const { chain } = useChains(); assert( msgValue.amount, "Amount must be set, same as https://github.com/osmosis-labs/telescope/issues/386", @@ -22,7 +22,7 @@ const TxMsgRedelegateDetails = ({ msgValue }: TxMsgRedelegateDetailsProps) => {
  • -
    {printableCoin(msgValue.amount, state.chain)}
    +
    {printableCoin(msgValue.amount, chain)}
  • diff --git a/components/dataViews/TransactionInfo/TxMsgSendDetails.tsx b/components/dataViews/TransactionInfo/TxMsgSendDetails.tsx index 3a34ac0..8a1561f 100644 --- a/components/dataViews/TransactionInfo/TxMsgSendDetails.tsx +++ b/components/dataViews/TransactionInfo/TxMsgSendDetails.tsx @@ -1,5 +1,5 @@ import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; -import { useAppContext } from "../../../context/AppContext"; +import { useChains } from "../../../context/ChainsContext"; import { printableCoins } from "../../../lib/displayHelpers"; import HashView from "../HashView"; @@ -8,7 +8,7 @@ interface TxMsgSendDetailsProps { } const TxMsgSendDetails = ({ msgValue }: TxMsgSendDetailsProps) => { - const { state } = useAppContext(); + const { chain } = useChains(); return ( <> @@ -17,7 +17,7 @@ const TxMsgSendDetails = ({ msgValue }: TxMsgSendDetailsProps) => {
  • -
    {printableCoins(msgValue.amount, state.chain)}
    +
    {printableCoins(msgValue.amount, chain)}
  • diff --git a/components/dataViews/TransactionInfo/TxMsgUndelegateDetails.tsx b/components/dataViews/TransactionInfo/TxMsgUndelegateDetails.tsx index df50bfb..bbfbac1 100644 --- a/components/dataViews/TransactionInfo/TxMsgUndelegateDetails.tsx +++ b/components/dataViews/TransactionInfo/TxMsgUndelegateDetails.tsx @@ -1,6 +1,6 @@ import { assert } from "@cosmjs/utils"; import { MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; -import { useAppContext } from "../../../context/AppContext"; +import { useChains } from "../../../context/ChainsContext"; import { printableCoin } from "../../../lib/displayHelpers"; import HashView from "../HashView"; @@ -9,7 +9,7 @@ interface TxMsgUndelegateDetailsProps { } const TxMsgUndelegateDetails = ({ msgValue: msg }: TxMsgUndelegateDetailsProps) => { - const { state } = useAppContext(); + const { chain } = useChains(); assert( msg.amount, "Amount must be set, same as https://github.com/osmosis-labs/telescope/issues/386", @@ -22,7 +22,7 @@ const TxMsgUndelegateDetails = ({ msgValue: msg }: TxMsgUndelegateDetailsProps)
  • -
    {printableCoin(msg.amount, state.chain)}
    +
    {printableCoin(msg.amount, chain)}
  • diff --git a/components/dataViews/TransactionInfo/index.tsx b/components/dataViews/TransactionInfo/index.tsx index 5986a8e..5e3a46a 100644 --- a/components/dataViews/TransactionInfo/index.tsx +++ b/components/dataViews/TransactionInfo/index.tsx @@ -1,5 +1,5 @@ import { EncodeObject } from "@cosmjs/proto-signing"; -import { useAppContext } from "../../../context/AppContext"; +import { useChains } from "../../../context/ChainsContext"; import { printableCoins } from "../../../lib/displayHelpers"; import { DbTransaction } from "../../../types"; import { MsgTypeUrls } from "../../../types/txMsg"; @@ -41,7 +41,7 @@ interface TransactionInfoProps { } const TransactionInfo = ({ tx }: TransactionInfoProps) => { - const { state } = useAppContext(); + const { chain } = useChains(); return ( <> @@ -56,7 +56,7 @@ const TransactionInfo = ({ tx }: TransactionInfoProps) => {
  • -
    {printableCoins(tx.fee.amount, state.chain)}
    +
    {printableCoins(tx.fee.amount, chain)}
  • ) : null} From f8067bf7d220b58f25d09b6d2c01320737cc650c Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 3 Jul 2023 08:20:17 +0200 Subject: [PATCH 08/21] Use new hook in forms --- .../MsgForm/MsgClaimRewardsForm.tsx | 22 ++++-------- .../MsgForm/MsgCreateVestingAccountForm.tsx | 32 +++++++---------- .../CreateTxForm/MsgForm/MsgDelegateForm.tsx | 30 +++++++--------- .../MsgForm/MsgRedelegateForm.tsx | 36 ++++++++----------- .../CreateTxForm/MsgForm/MsgSendForm.tsx | 31 +++++++--------- .../MsgForm/MsgSetWithdrawAddressForm.tsx | 22 ++++-------- .../CreateTxForm/MsgForm/MsgTransferForm.tsx | 12 +++---- .../MsgForm/MsgUndelegateForm.tsx | 30 +++++++--------- components/forms/CreateTxForm/index.tsx | 16 ++++----- components/forms/FindMultisigForm.tsx | 19 ++++------ components/forms/MultisigForm.tsx | 19 ++++------ components/forms/TransactionSigning.tsx | 35 ++++++++---------- 12 files changed, 115 insertions(+), 189 deletions(-) diff --git a/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx index f68ffde..88b8580 100644 --- a/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx +++ b/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx @@ -1,8 +1,7 @@ import { MsgWithdrawDelegatorRewardEncodeObject } from "@cosmjs/stargate"; -import { assert } from "@cosmjs/utils"; import { useEffect, useState } from "react"; import { MsgGetter } from ".."; -import { useAppContext } from "../../../../context/AppContext"; +import { useChains } from "../../../../context/ChainsContext"; import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg"; import Input from "../../../inputs/Input"; @@ -19,8 +18,7 @@ const MsgClaimRewardsForm = ({ setMsgGetter, deleteMsg, }: MsgClaimRewardsFormProps) => { - const { state } = useAppContext(); - assert(state.chain.addressPrefix, "addressPrefix missing"); + const { chain } = useChains(); const [validatorAddress, setValidatorAddress] = useState(""); const [validatorAddressError, setValidatorAddressError] = useState(""); @@ -30,12 +28,10 @@ const MsgClaimRewardsForm = ({ setValidatorAddressError(""); const isMsgValid = (): boolean => { - assert(state.chain.addressPrefix, "addressPrefix missing"); - - const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix); + const addressErrorMsg = checkAddress(validatorAddress, chain.addressPrefix); if (addressErrorMsg) { setValidatorAddressError( - `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, + `Invalid address for network ${chain.chainId}: ${addressErrorMsg}`, ); return false; } @@ -55,13 +51,7 @@ const MsgClaimRewardsForm = ({ setMsgGetter({ isMsgValid, msg }); } catch {} - }, [ - delegatorAddress, - setMsgGetter, - state.chain.addressPrefix, - state.chain.chainId, - validatorAddress, - ]); + }, [chain.addressPrefix, chain.chainId, delegatorAddress, setMsgGetter, validatorAddress]); return ( @@ -76,7 +66,7 @@ const MsgClaimRewardsForm = ({ value={validatorAddress} onChange={({ target }) => setValidatorAddress(target.value)} error={validatorAddressError} - placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`} + placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`} />