import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate' import { Decimal } from '@cosmjs/math' import type { OfflineSigner } from '@cosmjs/proto-signing' import type { Coin } from '@cosmjs/stargate' import type { AppConfig } from 'config' import { getConfig, keplrConfig } from 'config' import type { ReactNode } from 'react' import { useEffect } from 'react' import { toast } from 'react-hot-toast' import { createTrackedSelector } from 'react-tracked' import { NETWORK } from 'utils/constants' import type { State } from 'zustand' import create from 'zustand' import { subscribeWithSelector } from 'zustand/middleware' export interface KeplrWalletStore extends State { accountNumber: number address: string balance: Coin[] client: SigningCosmWasmClient | undefined config: AppConfig initialized: boolean initializing: boolean name: string network: string signer: OfflineSigner | undefined readonly clear: () => void readonly connect: (walletChange?: boolean | 'focus') => Promise readonly disconnect: () => void | Promise readonly getClient: () => SigningCosmWasmClient readonly getSigner: () => OfflineSigner readonly init: (signer?: OfflineSigner) => void readonly refreshBalance: (address?: string, balance?: Coin[]) => Promise readonly setNetwork: (network: string) => void readonly updateSigner: (singer: OfflineSigner) => void readonly setQueryClient: () => void } /** * Compatibility export for references still using `WalletContextType` * * @deprecated replace with {@link KeplrWalletStore} */ export type WalletContextType = KeplrWalletStore /** * Keplr wallet store default values as a separate variable for reusability */ const defaultStates = { accountNumber: 0, address: '', balance: [], client: undefined, config: getConfig(NETWORK), initialized: false, initializing: true, name: '', network: NETWORK, signer: undefined, } /** * Entrypoint for keplr wallet store using {@link defaultStates} */ export const useWalletStore = create( subscribeWithSelector((set, get) => ({ ...defaultStates, clear: () => set({ ...defaultStates }), connect: async (walletChange = false) => { try { if (walletChange !== 'focus') set({ initializing: true }) const { config, init } = get() const signer = await loadKeplrWallet(config) init(signer) if (walletChange) set({ initializing: false }) } catch (err: any) { toast.error(err?.message, { style: { maxWidth: 'none' } }) set({ initializing: false }) } }, disconnect: () => { window.localStorage.clear() get().clear() set({ initializing: false }) }, getClient: () => get().client!, getSigner: () => get().signer!, init: (signer) => set({ signer }), refreshBalance: async (address = get().address, balance = get().balance) => { const { client, config } = get() if (!client) return balance.length = 0 for (const denom in config.coinMap) { // eslint-disable-next-line no-await-in-loop const coin = await client.getBalance(address, denom) if (coin) balance.push(coin) } set({ balance }) }, setNetwork: (network) => set({ network }), updateSigner: (signer) => set({ signer }), setQueryClient: async () => { try { const client = (await createQueryClient()) as SigningCosmWasmClient set({ client }) } catch (err: any) { toast.error(err?.message, { style: { maxWidth: 'none' } }) set({ initializing: false }) } }, })), ) /** * Proxied keplr wallet store which only rerenders on called state values. * * Recommended if only consuming state; to set states, use {@link useWalletStore.setState}. * * @example * * ```ts * // this will rerender if any state values has changed * const { name } = useWalletStore() * * // this will rerender if only `name` has changed * const { name } = useWallet() * ``` */ export const useWallet = createTrackedSelector(useWalletStore) /** * Keplr wallet store provider to easily mount {@link WalletSubscription} * to listen/subscribe various state changes. * */ export const WalletProvider = ({ children }: { children: ReactNode }) => { return ( <> {children} ) } /** * Keplr wallet subscriptions (side effects) */ const WalletSubscription = () => { /** * Dispatch reconnecting wallet on first mount and register events to refresh * on keystore change and window refocus. * */ useEffect(() => { const walletAddress = window.localStorage.getItem('wallet_address') if (walletAddress) { void useWalletStore.getState().connect() } else { useWalletStore.setState({ initializing: false }) useWalletStore.getState().setQueryClient() } const listenChange = () => { void useWalletStore.getState().connect(true) } const listenFocus = () => { if (walletAddress) void useWalletStore.getState().connect('focus') } window.addEventListener('keplr_keystorechange', listenChange) window.addEventListener('focus', listenFocus) return () => { window.removeEventListener('keplr_keystorechange', listenChange) window.removeEventListener('focus', listenFocus) } }, []) /** * Watch signer changes to initialize client state. * */ useEffect(() => { return useWalletStore.subscribe( (x) => x.signer, // eslint-disable-next-line @typescript-eslint/no-misused-promises async (signer) => { try { if (!signer) { useWalletStore.setState({ client: (await createQueryClient()) as SigningCosmWasmClient, }) } else { useWalletStore.setState({ client: await createClient({ signer }), }) } } catch (error) { console.log(error) } }, ) }, []) /** * Watch client changes to refresh balance and sync wallet states. * */ useEffect(() => { return useWalletStore.subscribe( (x) => x.client, // eslint-disable-next-line @typescript-eslint/no-misused-promises async (client) => { const { config, refreshBalance, signer } = useWalletStore.getState() if (!signer || !client) return if (!window.keplr) { throw new Error('window.keplr not found') } const balance: Coin[] = [] const address = (await signer.getAccounts())[0].address const account = await client.getAccount(address) const key = await window.keplr.getKey(config.chainId) await refreshBalance(address, balance) window.localStorage.setItem('wallet_address', address) useWalletStore.setState({ accountNumber: account?.accountNumber || 0, address, balance, initialized: true, initializing: false, name: key.name || '', }) }, ) }, []) return null } /** * Function to create signing client based on {@link useWalletStore} resolved * config state. * * @param arg - Object argument requiring `signer` */ const createClient = ({ signer }: { signer: OfflineSigner }) => { const { config } = useWalletStore.getState() return SigningCosmWasmClient.connectWithSigner(config.rpcUrl, signer, { gasPrice: { amount: Decimal.fromUserInput('0.0025', 100), denom: config.feeToken, }, }) } const createQueryClient = () => { const { config } = useWalletStore.getState() return SigningCosmWasmClient.connect(config.rpcUrl) } /** * Function to load keplr wallet signer. * * @param config - Application configuration */ const loadKeplrWallet = async (config: AppConfig) => { if (!window.getOfflineSigner || !window.keplr || !window.getOfflineSignerAuto) { throw new Error('Keplr extension is not available') } await window.keplr.experimentalSuggestChain(keplrConfig(config)) await window.keplr.enable(config.chainId) const signer = await window.getOfflineSignerAuto(config.chainId) Object.assign(signer, { signAmino: (signer as any).signAmino ?? (signer as any).sign, }) return signer }