From 16ea529cb8bf361a0873e32d42c5962d79520408 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:58:35 +0100 Subject: [PATCH 01/17] Add explorerAccountLink --- components/ChainConnect/CustomChainForm.tsx | 28 +++++++++++++++++---- context/ChainsContext/helpers.tsx | 5 ++-- context/ChainsContext/storage.tsx | 10 ++++++-- context/ChainsContext/types.tsx | 7 +++++- lib/chainRegistry.ts | 22 ++++++++++++++-- lib/displayHelpers.spec.ts | 7 +++++- lib/displayHelpers.ts | 4 +-- pages/[chainName]/[address]/index.tsx | 7 ++---- types/chainRegistry.ts | 5 ++-- 9 files changed, 72 insertions(+), 23 deletions(-) diff --git a/components/ChainConnect/CustomChainForm.tsx b/components/ChainConnect/CustomChainForm.tsx index bb34867..7d5afde 100644 --- a/components/ChainConnect/CustomChainForm.tsx +++ b/components/ChainConnect/CustomChainForm.tsx @@ -37,7 +37,8 @@ export default function CustomChainForm() { bech32Prefix: z.string({ required_error: "Address prefix is required" }), gasPrice: z.string({ required_error: "Gas price is required" }), rpcNodes: z.string({ required_error: "Comma separated rpc nodes are required" }), - explorerLink: z.string({ required_error: "Explorer url is required" }), + explorerTxLink: z.string({ required_error: "Explorer tx url is required" }), + explorerAccountLink: z.string({ required_error: "Explorer account url is required" }), logo: z.string({ required_error: "Logo url is required" }), assets: z.string({ required_error: "Assets json is required" }), }) @@ -56,7 +57,8 @@ export default function CustomChainForm() { bech32Prefix: defaultChain.addressPrefix, gasPrice: defaultChain.gasPrice, rpcNodes: defaultChain.nodeAddresses.join(", "), - explorerLink: defaultChain.explorerLink, + explorerTxLink: defaultChain.explorerLink.tx, + explorerAccountLink: defaultChain.explorerLink.account, logo: defaultChain.logo, assets: JSON.stringify(defaultChain.assets), }, @@ -80,7 +82,10 @@ export default function CustomChainForm() { assets: JSON.parse(chainFromForm.assets) as RegistryAsset[], gasPrice: chainFromForm.gasPrice, addressPrefix: chainFromForm.bech32Prefix, - explorerLink: chainFromForm.explorerLink, + explorerLink: { + tx: chainFromForm.explorerTxLink, + account: chainFromForm.explorerAccountLink, + }, }, }); } @@ -190,10 +195,10 @@ export default function CustomChainForm() { )} /> ( - Explorer Link + Explorer Tx Link @@ -202,6 +207,19 @@ export default function CustomChainForm() { )} /> + ( + + Explorer Account Link + + + + with {"'${accountAddress}'"} included + + + )} + /> ): chain is ChainInfo => @@ -30,8 +30,7 @@ export const isChainInfoFilled = (chain: Partial): chain is ChainInfo chain.displayDenomExponent >= 0 && chain.assets?.length && chain.gasPrice && - chain.addressPrefix && - chain.explorerLink, + chain.addressPrefix, ); export const setChains = (dispatch: Dispatch, chains: ChainItems) => { diff --git a/context/ChainsContext/storage.tsx b/context/ChainsContext/storage.tsx index 1f8e96a..6aa3155 100644 --- a/context/ChainsContext/storage.tsx +++ b/context/ChainsContext/storage.tsx @@ -118,6 +118,7 @@ export const getChainFromUrl = (chainName: string) => { const nodeAddressesValue: readonly string[] = JSON.parse(nodeAddresses || "[]"); const assetsValue: readonly RegistryAsset[] = JSON.parse(assets || "[]"); + const explorerLinkValue: Partial = JSON.parse(explorerLink || "{}"); const urlChain: Partial = { registryName: chainName, @@ -132,7 +133,9 @@ export const getChainFromUrl = (chainName: string) => { ...(assetsValue.length && { assets: assetsValue }), ...(gasPrice && { gasPrice }), ...(addressPrefix && { addressPrefix }), - ...(explorerLink && { explorerLink }), + ...(explorerLink && { + explorerLink: { tx: explorerLinkValue.tx || "", account: explorerLinkValue.account || "" }, + }), }; return urlChain; @@ -158,6 +161,7 @@ export const getChainFromEnvfile = (chainName: string) => { const nodeAddressesValue: readonly string[] = JSON.parse(nodeAddresses || "[]"); const assetsValue: readonly RegistryAsset[] = JSON.parse(assets || "[]"); + const explorerLinkValue: Partial = JSON.parse(explorerLink || "{}"); const envfileChain: Partial = { registryName: chainName, @@ -172,7 +176,9 @@ export const getChainFromEnvfile = (chainName: string) => { ...(assetsValue.length && { assets: assetsValue }), ...(gasPrice && { gasPrice }), ...(addressPrefix && { addressPrefix }), - ...(explorerLink && { explorerLink }), + ...(explorerLinkValue && { + explorerLink: { tx: explorerLinkValue.tx || "", account: explorerLinkValue.account || "" }, + }), }; return envfileChain; diff --git a/context/ChainsContext/types.tsx b/context/ChainsContext/types.tsx index 543af8a..463bd59 100644 --- a/context/ChainsContext/types.tsx +++ b/context/ChainsContext/types.tsx @@ -33,9 +33,14 @@ export interface ChainInfo { readonly assets: readonly RegistryAsset[]; readonly gasPrice: string; readonly addressPrefix: string; - readonly explorerLink: string; + readonly explorerLink: ExplorerLink; } +export type ExplorerLink = { + readonly tx: string; + readonly account: string; +}; + export type NewConnection = | { readonly action: "edit"; diff --git a/lib/chainRegistry.ts b/lib/chainRegistry.ts index 2df591f..e3351db 100644 --- a/lib/chainRegistry.ts +++ b/lib/chainRegistry.ts @@ -1,5 +1,5 @@ import { isChainInfoFilled } from "@/context/ChainsContext/helpers"; -import { ChainInfo, ChainItems } from "@/context/ChainsContext/types"; +import { ChainInfo, ChainItems, ExplorerLink } from "@/context/ChainsContext/types"; import { GithubChainRegistryItem, RegistryAsset, RegistryChain } from "@/types/chainRegistry"; import { requestGhJson } from "./request"; @@ -123,7 +123,25 @@ const getChainInfoFromJsons = ( const firstAsset = cdnRegistryAssets[0]; const logo = getLogoUri(registryChain, firstAsset); const nodeAddresses = registryChain.apis?.rpc.map(({ address }) => address) ?? []; - const explorerLink = registryChain.explorers?.[0]?.tx_page ?? ""; + + let explorerLink: ExplorerLink = { tx: "", account: "" }; + + // Prefer same explorer for both tx and account links + for (const explorer of registryChain.explorers ?? []) { + if (explorer.tx_page && explorer.account_page) { + explorerLink = { tx: explorer.tx_page, account: explorer.account_page }; + break; + } + + if (!explorerLink.tx && explorer.tx_page) { + explorerLink = { ...explorerLink, tx: explorer.tx_page }; + } + + if (!explorerLink.account && explorer.account_page) { + explorerLink = { ...explorerLink, account: explorer.account_page }; + } + } + const firstAssetDenom = firstAsset.base; const displayUnit = firstAsset.denom_units.find((u) => u.denom == firstAsset.display); const displayDenom = displayUnit ? firstAsset.symbol : firstAsset.base; diff --git a/lib/displayHelpers.spec.ts b/lib/displayHelpers.spec.ts index 66ac39e..c93edc6 100644 --- a/lib/displayHelpers.spec.ts +++ b/lib/displayHelpers.spec.ts @@ -7,8 +7,13 @@ const testChainInfo: ChainInfo = { addressPrefix: "juno", chainId: "uni-6", chainDisplayName: "Juno Testnet", + logo: "https://raw.githubusercontent.com/cosmos/chain-registry/master/testnets/junotestnet/images/juno.svg", nodeAddress: "https://rpc.uni.junonetwork.io", - explorerLink: "https://testnet.ezstaking.tools/juno-testnet/txs/${txHash}", + nodeAddresses: ["https://rpc.uni.junonetwork.io"], + explorerLink: { + tx: "https://testnet.ezstaking.tools/juno-testnet/txs/${txHash}", + account: "https://testnet.app.ezstaking.io/juno-testnet/account/${accountAddress}", + }, denom: "ujunox", displayDenom: "JUNOX", displayDenomExponent: 6, diff --git a/lib/displayHelpers.ts b/lib/displayHelpers.ts index 095c274..d69b45c 100644 --- a/lib/displayHelpers.ts +++ b/lib/displayHelpers.ts @@ -162,8 +162,8 @@ const explorerLinkTx = (link: string, hash: string) => { * for accounts. Returns null otherwise. */ const explorerLinkAccount = (link: string, address: string) => { - if (link && link.includes("${address}")) { - return link.replace("${address}", address); + if (link && link.includes("${accountAddress}")) { + return link.replace("${accountAddress}", address); } return null; }; diff --git a/pages/[chainName]/[address]/index.tsx b/pages/[chainName]/[address]/index.tsx index 5c4df27..71c09bd 100644 --- a/pages/[chainName]/[address]/index.tsx +++ b/pages/[chainName]/[address]/index.tsx @@ -29,10 +29,7 @@ const Multipage = () => { const [accountError, setAccountError] = useState(null); const multisigAddress = router.query.address?.toString(); - const explorerHref = explorerLinkAccount( - process.env.NEXT_PUBLIC_EXPLORER_LINK_ACCOUNT || "", - multisigAddress || "", - ); + const explorerLink = explorerLinkAccount(chain.explorerLink.account, multisigAddress || ""); const fetchMultisig = useCallback( async (address: string) => { @@ -70,7 +67,7 @@ const Multipage = () => {

{multisigAddress ? : "No Address"}

- {explorerHref ? : null} + {explorerLink ? : null}
{pubkey ? ( Date: Fri, 1 Dec 2023 15:59:02 +0100 Subject: [PATCH 02/17] Extract signing types --- components/forms/TransactionSigning.tsx | 9 +-------- types/signing.ts | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 types/signing.ts diff --git a/components/forms/TransactionSigning.tsx b/components/forms/TransactionSigning.tsx index 220bd51..f28bd4e 100644 --- a/components/forms/TransactionSigning.tsx +++ b/components/forms/TransactionSigning.tsx @@ -1,3 +1,4 @@ +import { LoadingStates, SigningStatus } from "@/types/signing"; import { MultisigThresholdPubkey, makeCosmoshubPath } from "@cosmjs/amino"; import { createWasmAminoConverters, wasmTypes } from "@cosmjs/cosmwasm-stargate"; import { toBase64 } from "@cosmjs/encoding"; @@ -20,14 +21,6 @@ import HashView from "../dataViews/HashView"; import Button from "../inputs/Button"; import StackableContainer from "../layout/StackableContainer"; -type SigningStatus = "not_signed" | "not_a_member" | "signed"; - -interface LoadingStates { - readonly signing?: boolean; - readonly keplr?: boolean; - readonly ledger?: boolean; -} - interface TransactionSigningProps { readonly signatures: DbSignature[]; readonly tx: DbTransaction; diff --git a/types/signing.ts b/types/signing.ts new file mode 100644 index 0000000..09b7feb --- /dev/null +++ b/types/signing.ts @@ -0,0 +1,15 @@ +export type SigningStatus = "not_signed" | "not_a_member" | "signed"; + +export type WalletType = "Keplr" | "Ledger"; + +export interface WalletInfo { + readonly type: WalletType; + readonly address: string; + readonly pubKey: string; +} + +export interface LoadingStates { + readonly signing?: boolean; + readonly keplr?: boolean; + readonly ledger?: boolean; +} From ca85ce1328ac3714d1155f352aa91e16813d747d Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:00:04 +0100 Subject: [PATCH 03/17] Use new explorer link --- components/dataViews/CompletedTransaction.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/dataViews/CompletedTransaction.tsx b/components/dataViews/CompletedTransaction.tsx index ec49e71..0665607 100644 --- a/components/dataViews/CompletedTransaction.tsx +++ b/components/dataViews/CompletedTransaction.tsx @@ -10,8 +10,8 @@ interface CompletedTransactionProps { const CompletedTransaction = ({ transactionHash }: CompletedTransactionProps) => { const { chain } = useChains(); - const baseURL = chain.explorerLink ? chain.explorerLink : ""; - const explorerLink = explorerLinkTx(baseURL, transactionHash); + const explorerLink = explorerLinkTx(chain.explorerLink.tx, transactionHash); + return ( From 1461f601d322039ffcd19aab217e0e94d2346e7d Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:01:12 +0100 Subject: [PATCH 04/17] Redirect from / to /chainName --- context/ChainsContext/storage.tsx | 39 ++++++++++++++----------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/context/ChainsContext/storage.tsx b/context/ChainsContext/storage.tsx index 6aa3155..534969c 100644 --- a/context/ChainsContext/storage.tsx +++ b/context/ChainsContext/storage.tsx @@ -1,6 +1,6 @@ import { RegistryAsset } from "@/types/chainRegistry"; import { emptyChain } from "./helpers"; -import { ChainInfo, ChainItems } from "./types"; +import { ChainInfo, ChainItems, ExplorerLink } from "./types"; const registryShaStorageKey = "context-registry-sha"; export const getShaFromStorage = () => localStorage.getItem(registryShaStorageKey); @@ -99,7 +99,7 @@ export const getRecentChainFromStorage = (chains: ChainItems): Partial { if (!chainName) { - return emptyChain; + return { registryName: chainName }; } const params = new URLSearchParams(location.search); @@ -144,7 +144,7 @@ export const getChainFromUrl = (chainName: string) => { export const getChainFromEnvfile = (chainName: string) => { const registryName = process.env.NEXT_PUBLIC_REGISTRY_NAME || ""; if (chainName && registryName !== chainName) { - return emptyChain; + return { registryName: chainName }; } const logo = process.env.NEXT_PUBLIC_LOGO; @@ -198,31 +198,26 @@ export const getChainFromStorage = ( }; export const setChainInUrl = (chain: ChainInfo, chains: ChainItems) => { - const params = new URLSearchParams(); - const storedChain = getChainFromStorage(chain.registryName, chains); + const newPathname = location.pathname.includes(chain.registryName) + ? location.pathname + : `/${chain.registryName}`; + + if (chains.mainnets.has(chain.registryName) || chains.testnets.has(chain.registryName)) { + window.history.replaceState({}, "", newPathname); + return; + } // Set full url if chain is not on chain-registry repo - if (!storedChain || chains.localnets.has(chain.registryName)) { - for (const [key, value] of Object.entries(chain)) { - if (typeof value === "object") { - params.set(key, JSON.stringify(value)); - } else { - params.set(key, value); - } - } - } else { - for (const [key, value] of Object.entries(chain)) { - const storedValue = storedChain[key as keyof ChainInfo]; + const params = new URLSearchParams(); - if (typeof value === "object" && JSON.stringify(value) !== JSON.stringify(storedValue)) { - params.set(key, JSON.stringify(value)); - } else if (value !== storedValue) { - params.set(key, value); - } + for (const [key, value] of Object.entries(chain)) { + if (typeof value === "object") { + params.set(key, JSON.stringify(value)); + } else { + params.set(key, value); } } - const newPathname = location.pathname.includes(chain.registryName) ? location.pathname : "/"; const newUrl = params.size ? `${newPathname}?${params}` : newPathname; window.history.replaceState({}, "", newUrl); From 7d629835bb06feb546e5a0745d36f003634fc6ef Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Fri, 1 Dec 2023 16:03:09 +0100 Subject: [PATCH 05/17] Redirect from multipage index to chainName. Add Skeleton --- components/forms/FindMultisigForm.tsx | 5 ++- pages/index.tsx | 47 +++++++++++++++++++-------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/components/forms/FindMultisigForm.tsx b/components/forms/FindMultisigForm.tsx index bba3adf..98841aa 100644 --- a/components/forms/FindMultisigForm.tsx +++ b/components/forms/FindMultisigForm.tsx @@ -69,7 +69,10 @@ const FindMultisigForm = (props: Props) => {

Don't have a multisig?

-