From 019809a97f3e438dce4f3c9a88f4d96e3fe01a5f Mon Sep 17 00:00:00 2001 From: Alex D Date: Fri, 20 Sep 2024 12:14:30 +0200 Subject: [PATCH] feat(spaceward): technical debt (#868) * add more evm chains * fix https://github.com/warden-protocol/wardenprotocol/issues/864 * fixup asset balances query * handle UpdateSpaceMessage * add snap version check * add missing env --- spaceward/entrypoint.sh | 1 + spaceward/src/config/tokens.ts | 11 +- spaceward/src/env.ts | 3 + .../src/features/actions/StatusSidebar.tsx | 6 +- spaceward/src/features/actions/util.ts | 58 +++++++--- spaceward/src/features/assets/hooks.ts | 104 +++++++++++------- spaceward/src/features/assets/queries.ts | 32 ++++-- .../metamask/InstallMetaMaskSnapButton.tsx | 2 +- .../src/features/modals/AssetSelector.tsx | 28 ++--- spaceward/src/hooks/useMetaMaskRequestSnap.ts | 2 +- spaceward/src/lib/eth/constants.ts | 16 ++- spaceward/src/lib/metamask.ts | 24 ++-- spaceward/src/pages/Assets.tsx | 3 + 13 files changed, 195 insertions(+), 95 deletions(-) diff --git a/spaceward/entrypoint.sh b/spaceward/entrypoint.sh index 675498570..46b8bcbcd 100644 --- a/spaceward/entrypoint.sh +++ b/spaceward/entrypoint.sh @@ -28,6 +28,7 @@ if [ "$1" = 'nginx-fe' ]; then replace_var WARDEN_COSMOSKIT_CHAIN_NAME "$filename" replace_var WARDEN_MAINTENANCE "$filename" replace_var WARDEN_SNAP_ORIGIN "$filename" + replace_var WARDEN_SNAP_VERSION "$filename" replace_var WARDEN_ENVIRONMENT "$filename" replace_var WARDEN_STORYBLOK_TOKEN "$filename" replace_var WARDEN_ETHEREUM_ANALYZER_CONTRACT "$filename" diff --git a/spaceward/src/config/tokens.ts b/spaceward/src/config/tokens.ts index 5a48daac0..b110db197 100644 --- a/spaceward/src/config/tokens.ts +++ b/spaceward/src/config/tokens.ts @@ -216,19 +216,16 @@ export const COSMOS_PRICES: Record = { OSMO: BigInt(0.4446 * 10 ** 8), }; -/* TODO networks -Astar native ASTR -Avalanche native AVAX -Binance Coin native BNB -Ethereum Classic native ETC -Polygon native MATIC -*/ const _ENABLED_ETH_CHAINS: { chainName: ChainName; testnet?: boolean }[] = [ { chainName: "arbitrum" }, + { chainName: "astar" }, + { chainName: "avalanche" }, { chainName: "base" }, { chainName: "bsc" }, + { chainName: "ethereumClassic" }, { chainName: "mainnet" }, { chainName: "optimism" }, + { chainName: "polygon" }, { chainName: "sepolia", testnet: true }, ]; diff --git a/spaceward/src/env.ts b/spaceward/src/env.ts index da7d8b107..d32873bef 100644 --- a/spaceward/src/env.ts +++ b/spaceward/src/env.ts @@ -10,6 +10,8 @@ const chainId = import.meta.env.VITE_WARDEN_CHAIN_ID || "warden"; const maintenance = import.meta.env.VITE_WARDEN_MAINTENANCE || false; const snapOrigin = import.meta.env.VITE_WARDEN_SNAP_ORIGIN || "local:http://localhost:8123"; +const snapVersion = + import.meta.env.VITE_WARDEN_SNAP_VERSION || "0.1.5"; const spacewardEnv = import.meta.env.VITE_WARDEN_ENVIRONMENT || "development"; // development, production const storyblokToken = import.meta.env.VITE_WARDEN_STORYBLOK_TOKEN || "LTh76K2yz5nU6jUThhFG3Qtt"; @@ -34,6 +36,7 @@ export const env = { chainId, maintenance, snapOrigin, + snapVersion, spacewardEnv, storyblokToken, cosmoskitChainName, diff --git a/spaceward/src/features/actions/StatusSidebar.tsx b/spaceward/src/features/actions/StatusSidebar.tsx index 5a621cb4c..01f2f88a4 100644 --- a/spaceward/src/features/actions/StatusSidebar.tsx +++ b/spaceward/src/features/actions/StatusSidebar.tsx @@ -20,6 +20,7 @@ import { QueuedAction, QueuedActionStatus, useActionsState } from "./hooks"; import { getActionHandler, GetStatus, handleCosmos, handleEth, handleEthRaw } from "./util"; import { TEMP_KEY, useKeySettingsState } from "../keys/state"; import Assets from "../keys/assets"; +import { useQueryClient } from "@tanstack/react-query"; interface ItemProps extends QueuedAction { single?: boolean; @@ -43,6 +44,7 @@ const waitForVisibility = () => { }; function ActionItem({ single, ...item }: ItemProps) { + const queryClient = useQueryClient(); const { walletManager } = useContext(walletContext); const { data: ks, setData: setKeySettings } = useKeySettingsState(); const { toast } = useToast() @@ -307,9 +309,9 @@ function ActionItem({ single, ...item }: ItemProps) { if (item.networkType === "eth-raw") { res = await handleEthRaw({ action: item, w }); } else if (item.networkType === "eth") { - res = await handleEth({ action: item, w }); + res = await handleEth({ action: item, w, queryClient }); } else if (item.networkType === "cosmos") { - res = await handleCosmos({ action: item, w, walletManager }); + res = await handleCosmos({ action: item, w, walletManager, queryClient }); } } catch (e) { console.error("broadcast failed", e); diff --git a/spaceward/src/features/actions/util.ts b/spaceward/src/features/actions/util.ts index 2f8a7f568..6842b72e9 100644 --- a/spaceward/src/features/actions/util.ts +++ b/spaceward/src/features/actions/util.ts @@ -1,17 +1,19 @@ +import { hexlify, Transaction } from "ethers"; +import { WalletManager } from "@cosmos-kit/core"; +import { isDeliverTxSuccess, StargateClient } from "@cosmjs/stargate"; +import { KeyringSnapRpcClient } from "@metamask/keyring-api"; +import type { QueryClient } from "@tanstack/react-query"; +import { IWeb3Wallet } from "@walletconnect/web3wallet"; import { cosmos, warden } from "@wardenprotocol/wardenjs"; +import { base64FromBytes } from "@wardenprotocol/wardenjs/codegen/helpers"; import { env } from "@/env"; +import { COSMOS_CHAINS } from "@/config/tokens"; import type { getClient } from "@/hooks/useClient"; -import type { QueuedAction } from "./hooks"; -import { IWeb3Wallet } from "@walletconnect/web3wallet"; -import { hexlify, Transaction } from "ethers"; -import { KeyringSnapRpcClient } from "@metamask/keyring-api"; +import { getBalanceQueryKey } from "@/features/assets/queries"; import { getProvider, isSupportedNetwork } from "@/lib/eth"; -import { COSMOS_CHAINS } from "@/config/tokens"; import { isUint8Array } from "@/lib/utils"; -import { base64FromBytes } from "@wardenprotocol/wardenjs/codegen/helpers"; +import type { QueuedAction } from "./hooks"; import { prepareTx } from "../modals/util"; -import { WalletManager } from "@cosmos-kit/core"; -import { isDeliverTxSuccess, StargateClient } from "@cosmjs/stargate"; export type GetStatus = ( client: Awaited>, @@ -130,6 +132,15 @@ export const getActionHandler = ({ }; break; } + case warden.warden.v1beta3.MsgUpdateSpaceResponse.typeUrl: { + getStatus = async () => ({ + pending: false, + error: false, + done: true, + }); + + break; + } default: throw new Error(`action type not implemented: ${typeUrl}`); } @@ -203,9 +214,11 @@ export const handleEthRaw = async ({ export const handleEth = async ({ action, w, + queryClient, }: { action: QueuedAction; w: IWeb3Wallet | null; + queryClient: QueryClient; }) => { const { chainName, @@ -269,22 +282,25 @@ export const handleEth = async ({ .then(() => true); } - return ( - provider - .waitForTransaction(res.hash) - // fixme - .then(() => true) - ); + return provider.waitForTransaction(res.hash).then(() => { + queryClient.invalidateQueries({ + queryKey: getBalanceQueryKey("eip155", chainName, "").slice(0, -1), + }); + + return true; + }); }; export const handleCosmos = async ({ action, w, walletManager, + queryClient, }: { action: QueuedAction; w: IWeb3Wallet | null; walletManager: WalletManager; + queryClient: QueryClient; }) => { const { chainName, @@ -343,7 +359,19 @@ export const handleCosmos = async ({ }, }) // fixme - .then(() => true); + .then(() => { + // todo + // possibly add a timeout, as walletconnect with cosmos does not wait for tx result + + queryClient.invalidateQueries({ + queryKey: getBalanceQueryKey("cosmos", chainName, "").slice( + 0, + -1, + ), + }); + + return true; + }); } const { signedTxBodyBytes, signedAuthInfoBytes } = prepareTx( diff --git a/spaceward/src/features/assets/hooks.ts b/spaceward/src/features/assets/hooks.ts index 19a98c54c..93fc8cd38 100644 --- a/spaceward/src/features/assets/hooks.ts +++ b/spaceward/src/features/assets/hooks.ts @@ -1,26 +1,85 @@ -import { useQueryHooks } from "@/hooks/useClient"; +import { walletContext } from "@cosmos-kit/react-lite"; +import type { ExtendedHttpEndpoint, WalletManager } from "@cosmos-kit/core"; import { AddressType } from "@wardenprotocol/wardenjs/codegen/warden/warden/v1beta3/key"; import { cosmos } from "@wardenprotocol/wardenjs"; -import { useQueries } from "@tanstack/react-query"; +import { useQueries, useQuery } from "@tanstack/react-query"; import { useContext, useEffect, useMemo, useState } from "react"; +import { COSMOS_CHAINS } from "@/config/tokens"; +import { useQueryHooks } from "@/hooks/useClient"; import { balancesQueryCosmos, balancesQueryEth, fiatPricesQuery, } from "./queries"; import type { CosmosQueryClient, PriceMapSlinky } from "./types"; -import { COSMOS_CHAINS } from "@/config/tokens"; -import { walletContext } from "@cosmos-kit/react-lite"; -import { ExtendedHttpEndpoint } from "@cosmos-kit/core"; const DERIVE_ADDRESSES = [ AddressType.ADDRESS_TYPE_ETHEREUM, AddressType.ADDRESS_TYPE_OSMOSIS, ]; + +const queryCosmosClients = (walletManager: WalletManager) => { + const rpcClients: Record = {}; + const rpcRetry: Record = {}; + + return { + queryKey: ["cosmos", "rpcClients"], + queryFn: async () => { + const clients: [CosmosQueryClient, string][] = []; + + for (let i = 0; i < COSMOS_CHAINS.length; i++) { + const { chainName, rpc } = COSMOS_CHAINS[i]; + let client = rpcClients[chainName]; + + if (client) { + // todo implement client health check + clients.push([client, chainName]); + continue; + } + + let endpoint: ExtendedHttpEndpoint | string; + + if (rpc) { + const retry = rpcRetry[chainName] ?? 0; + endpoint = rpc[retry % rpc.length]; + rpcRetry[chainName] = retry + 1; + } else { + const repo = walletManager.getWalletRepo(chainName); + repo.activate(); + + try { + endpoint = await repo.getRpcEndpoint(); + } catch (e) { + console.error(e); + endpoint = `https://rpc.cosmos.directory/${chainName}`; + } + } + + try { + const client = + await cosmos.ClientFactory.createRPCQueryClient({ + rpcEndpoint: + typeof endpoint === "string" + ? endpoint + : endpoint.url, + }); + + clients.push([client, chainName]); + } catch (e) { + console.error(e); + continue; + } + } + + return clients; + }, + } as const; +}; + export const useAssetQueries = (spaceId?: string | null) => { const { walletManager } = useContext(walletContext); const { isReady, useKeysBySpaceId, slinky } = useQueryHooks(); - const [clients, setClients] = useState<[CosmosQueryClient, string][]>([]); + const clients = useQuery(queryCosmosClients(walletManager)).data; const pairs = slinky.oracle.v1.useGetAllCurrencyPairs({ options: { enabled: isReady, refetchInterval: Infinity }, @@ -83,39 +142,6 @@ export const useAssetQueries = (spaceId?: string | null) => { return priceMap; }, [prices.data, currencyPairs]); - useEffect(() => { - Promise.all( - COSMOS_CHAINS.map(({ chainName, rpc }) => { - let promise: Promise; - - if (!rpc) { - const repo = walletManager.getWalletRepo(chainName); - repo.activate(); - promise = repo.getRpcEndpoint(); - } else { - promise = Promise.resolve(rpc[0]); - } - - return promise - .then((endpoint) => - cosmos.ClientFactory.createRPCQueryClient({ - rpcEndpoint: endpoint - ? typeof endpoint === "string" - ? endpoint - : endpoint.url - : `https://rpc.cosmos.directory/${chainName}`, - }), - ) - .then( - (client) => - [client, chainName] as [CosmosQueryClient, string], - ); - }), - ).then((clients) => { - setClients(clients); - }); - }, []); - const queryKeys = useKeysBySpaceId({ request: { spaceId: spaceId ? BigInt(spaceId) : BigInt(0), diff --git a/spaceward/src/features/assets/queries.ts b/spaceward/src/features/assets/queries.ts index 90badf647..c2457b550 100644 --- a/spaceward/src/features/assets/queries.ts +++ b/spaceward/src/features/assets/queries.ts @@ -52,6 +52,14 @@ const getAsset = (chainAssets: AssetList, denom: string) => { return asset; }; +export const getBalanceQueryKey = ( + chainType: "cosmos" | "eip155", + chainName: string, + address: string, +) => { + return ["balance", chainType, chainName, address]; +}; + const cosmosBalancesQuery = (params: { address?: string; enabled: boolean; @@ -60,7 +68,11 @@ const cosmosBalancesQuery = (params: { prices?: PriceMapSlinky; }) => ({ enabled: params.enabled, - queryKey: ["cosmos", params.chainName, "balance", params.address], + queryKey: getBalanceQueryKey( + "cosmos", + params.chainName, + params.address ?? "", + ), queryFn: async () => { if (!params.address) { throw new Error("Address is required"); @@ -174,7 +186,10 @@ const eip155NativeBalanceQuery = ({ address?: `0x${string}`; prices?: PriceMapSlinky; }) => ({ - queryKey: ["eip155", chainName, "native", address], + queryKey: [ + ...getBalanceQueryKey("eip155", chainName, address ?? ""), + "native", + ], queryFn: async (): Promise => { if (!address) { throw new Error("Address is required"); @@ -199,10 +214,10 @@ const eip155NativeBalanceQuery = ({ const price: bigint = slinkyPrice ? BigInt(slinkyPrice.price?.price ?? 0) - : (priceFeedContract + : ((priceFeedContract ? await priceFeedContract.latestRoundData() : undefined - )?.answer ?? BigInt(0); + )?.answer ?? BigInt(0)); const priceDecimals = slinkyPrice ? Number(slinkyPrice.decimals) : 8; @@ -255,7 +270,10 @@ const eip155ERC20BalanceQuery = ({ prices?: PriceMapSlinky; }) => ({ enabled: enabled && Boolean(address && token), - queryKey: ["eip155", chainName, "erc20", address, token], + queryKey: [ + ...getBalanceQueryKey("eip155", chainName, address ?? ""), + `erc20:${token}`, + ], queryFn: async (): Promise => { if (!address || !token) { throw new Error("Address and token are required"); @@ -353,10 +371,10 @@ const eip155ERC20BalanceQuery = ({ ? BigInt("100000000") : slinkyPrice ? BigInt(slinkyPrice.price?.price ?? 0) - : (priceFeedContract + : ((priceFeedContract ? await priceFeedContract.latestRoundData() : undefined - )?.answer ?? BigInt(0); + )?.answer ?? BigInt(0)); const priceDecimals = stablecoin ? 8 diff --git a/spaceward/src/features/metamask/InstallMetaMaskSnapButton.tsx b/spaceward/src/features/metamask/InstallMetaMaskSnapButton.tsx index 11c96d921..f4185c55f 100644 --- a/spaceward/src/features/metamask/InstallMetaMaskSnapButton.tsx +++ b/spaceward/src/features/metamask/InstallMetaMaskSnapButton.tsx @@ -38,7 +38,7 @@ export function InstallMetaMaskSnapButton({ isReady, isReconnect }: InstallMetam src="/logos/metamask.svg" className="object-fill w-6 h-6 aspect-square" /> - {isReconnect ? "Reinstall snap" : "Install snap"} + {isReconnect ? "Update snap" : "Install snap"} ); } diff --git a/spaceward/src/features/modals/AssetSelector.tsx b/spaceward/src/features/modals/AssetSelector.tsx index 11990ff1b..2affe7587 100644 --- a/spaceward/src/features/modals/AssetSelector.tsx +++ b/spaceward/src/features/modals/AssetSelector.tsx @@ -282,17 +282,17 @@ const AssetSelector = ({ bigintToFloat( fiatConversion ? (item.balance * - item.price * - BigInt( - 10, - ) ** - BigInt( - fiatConversion.decimals, - )) / - fiatConversion.value + item.price * + BigInt( + 10, + ) ** + BigInt( + fiatConversion.decimals, + )) / + fiatConversion.value : BigInt(0), item.decimals + - item.priceDecimals, + item.priceDecimals, ), )} @@ -303,10 +303,12 @@ const AssetSelector = ({ network={item.chainName} className="invert dark:invert-0" /> - {item.chainName - .charAt(0) - .toUpperCase() + - item.chainName.slice(1)} + + {item.chainName + .charAt(0) + .toUpperCase() + + item.chainName.slice(1)} + ); diff --git a/spaceward/src/hooks/useMetaMaskRequestSnap.ts b/spaceward/src/hooks/useMetaMaskRequestSnap.ts index f7bb7da4b..388d18e1b 100644 --- a/spaceward/src/hooks/useMetaMaskRequestSnap.ts +++ b/spaceward/src/hooks/useMetaMaskRequestSnap.ts @@ -13,7 +13,7 @@ import useMetaMaskContext from "./useMetaMaskContext"; */ export const useMetaMaskRequestSnap = ( snapId = env.snapOrigin, - version?: string, + version = env.snapVersion, ) => { const request = useMetaMaskRequest(); const { setInstalledSnap } = useMetaMaskContext(); diff --git a/spaceward/src/lib/eth/constants.ts b/spaceward/src/lib/eth/constants.ts index 57c790328..46ff6eefe 100644 --- a/spaceward/src/lib/eth/constants.ts +++ b/spaceward/src/lib/eth/constants.ts @@ -15,17 +15,29 @@ export const ETH_CHAIN_CONFIG: Record< ], token: "BNB", }, + "61": { + rpc: ["https://etc.etcdesktop.com", "https://geth-at.etc-network.info"], + token: "ETC", + }, "137": { rpc: ["https://polygon-rpc.com/", "https://polygon.llamarpc.com"], + token: "MATIC", }, "324": { rpc: ["https://mainnet.era.zksync.io/"] }, "420": { rpc: ["https://goerli.optimism.io"] }, + "592": { + rpc: ["https://astar-rpc.dwellir.com", "https://1rpc.io/astr"], + token: "ASTR", + }, "8453": { rpc: ["https://mainnet.base.org/", "https://base.llamarpc.com"] }, "42161": { rpc: ["https://arb1.arbitrum.io/rpc", "https://arbitrum.llamarpc.com"], }, "42220": { rpc: ["https://forno.celo.org"] }, - "43114": { rpc: ["https://api.avax.network/ext/bc/C/rpc"] }, + "43114": { + rpc: ["https://api.avax.network/ext/bc/C/rpc"], + token: "AVAX", + }, "44787": { rpc: ["https://alfajores-forno.celo-testnet.org"] }, "80001": { rpc: ["https://rpc-mumbai.maticvigil.com"] }, "81457": { rpc: ["https://rpc.blast.io/"] }, @@ -46,9 +58,11 @@ export const ETH_CHAINID_MAP = { goerli: "5", optimism: "10", bsc: "56", + ethereumClassic: "61", polygon: "137", zksync: "324", optimismGoerli: "420", + astar: "592", base: "8453", arbitrum: "42161", celo: "42220", diff --git a/spaceward/src/lib/metamask.ts b/spaceward/src/lib/metamask.ts index 0f246a05e..3d1dc3224 100644 --- a/spaceward/src/lib/metamask.ts +++ b/spaceward/src/lib/metamask.ts @@ -1,4 +1,8 @@ -import { EIP6963AnnounceProviderEvent, MetaMaskInpageProvider } from "@metamask/providers"; +import { env } from "@/env"; +import { + EIP6963AnnounceProviderEvent, + MetaMaskInpageProvider, +} from "@metamask/providers"; export type GetSnapsResponse = Record; @@ -21,7 +25,7 @@ export async function hasSnapsSupport( ) { try { await provider.request({ - method: 'wallet_getSnaps', + method: "wallet_getSnaps", }); return true; @@ -51,7 +55,7 @@ export async function getMetaMaskEIP6963Provider() { */ function resolve(provider: MetaMaskInpageProvider | null) { window.removeEventListener( - 'eip6963:announceProvider', + "eip6963:announceProvider", onAnnounceProvider, ); clearTimeout(timeout); @@ -69,14 +73,14 @@ export async function getMetaMaskEIP6963Provider() { function onAnnounceProvider({ detail }: EIP6963AnnounceProviderEvent) { const { info, provider } = detail; - if (info.rdns.includes('io.metamask')) { + if (info.rdns.includes("io.metamask")) { resolve(provider); } } - window.addEventListener('eip6963:announceProvider', onAnnounceProvider); + window.addEventListener("eip6963:announceProvider", onAnnounceProvider); - window.dispatchEvent(new Event('eip6963:requestProvider')); + window.dispatchEvent(new Event("eip6963:requestProvider")); }); } @@ -87,7 +91,7 @@ export async function getMetaMaskEIP6963Provider() { * @returns The provider, or `null` if no provider supports snaps. */ export async function getSnapsProvider() { - if (typeof window === 'undefined') { + if (typeof window === "undefined") { return null; } @@ -126,7 +130,9 @@ export async function getSnapsProvider() { * @param snapId - The snap ID. * @returns True if it's a local Snap, or false otherwise. */ -export const isLocalSnap = (snapId: string) => snapId.startsWith('local:'); +export const isLocalSnap = (snapId: string) => snapId.startsWith("local:"); export const shouldDisplayReconnectButton = (installedSnap: Snap | null) => - installedSnap && isLocalSnap(installedSnap?.id); + installedSnap && isLocalSnap(installedSnap?.id) + ? true + : installedSnap?.version !== env.snapVersion; diff --git a/spaceward/src/pages/Assets.tsx b/spaceward/src/pages/Assets.tsx index 79f87aa6f..e6528dcc3 100644 --- a/spaceward/src/pages/Assets.tsx +++ b/spaceward/src/pages/Assets.tsx @@ -36,6 +36,7 @@ interface AssetPageState { } export function AssetsPage() { + const [state, dispatch] = useReducer(commonReducer, { keyFilter: "", networkFilter: "", @@ -49,6 +50,8 @@ export function AssetsPage() { const formatter = FIAT_FORMAT[currency]; const { setData: setModal } = useModalState(); const { spaceId } = useSpaceId(); + // fixme we have a situation where balances did not fetch on this page but were ok in receive modal + // possible caused error did cancel query update? const { queryKeys, queryBalances, queryPrices } = useAssetQueries(spaceId); const _results = queryBalances