stargaze-studio/contexts/wallet.tsx
2022-11-02 10:53:17 +03:00

290 lines
8.1 KiB
TypeScript

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<void>
readonly disconnect: () => void | Promise<void>
readonly getClient: () => SigningCosmWasmClient
readonly getSigner: () => OfflineSigner
readonly init: (signer?: OfflineSigner) => void
readonly refreshBalance: (address?: string, balance?: Coin[]) => Promise<void>
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<KeplrWalletStore>((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<KeplrWalletStore>(useWalletStore)
/**
* Keplr wallet store provider to easily mount {@link WalletSubscription}
* to listen/subscribe various state changes.
*
*/
export const WalletProvider = ({ children }: { children: ReactNode }) => {
return (
<>
{children}
<WalletSubscription />
</>
)
}
/**
* 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
}