From 029d26056994664331d5bf69753885bf98616d2c Mon Sep 17 00:00:00 2001 From: luca Date: Thu, 19 Sep 2024 16:45:17 +0800 Subject: [PATCH] feat: add contract address input field --- .../components/contract/CodeIdField.tsx | 4 +- .../contract/ContractAddressField.tsx | 148 ++++++++++++++++++ .../components/contract/QueryTab.tsx | 48 +++++- 3 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 examples/chain-template/components/contract/ContractAddressField.tsx diff --git a/examples/chain-template/components/contract/CodeIdField.tsx b/examples/chain-template/components/contract/CodeIdField.tsx index f637d853..3e294274 100644 --- a/examples/chain-template/components/contract/CodeIdField.tsx +++ b/examples/chain-template/components/contract/CodeIdField.tsx @@ -7,7 +7,7 @@ import { useCodeDetails } from '@/hooks'; import { useChainStore } from '@/contexts'; import { CodeInfo, isValidCodeId, resolvePermission } from '@/utils'; -type Status = { +export type InputStatus = { state: 'init' | 'loading' | 'success' | 'error'; message?: string; }; @@ -25,7 +25,7 @@ export const CodeIdField = ({ readonly?: boolean; defaultCodeId?: string; }) => { - const [status, setStatus] = useState({ state: 'init' }); + const [status, setStatus] = useState({ state: 'init' }); const { selectedChain } = useChainStore(); const { address } = useChain(selectedChain); diff --git a/examples/chain-template/components/contract/ContractAddressField.tsx b/examples/chain-template/components/contract/ContractAddressField.tsx new file mode 100644 index 00000000..b745a6e5 --- /dev/null +++ b/examples/chain-template/components/contract/ContractAddressField.tsx @@ -0,0 +1,148 @@ +import { + Box, + Combobox, + Icon, + Spinner, + Text, + TextProps, +} from '@interchain-ui/react'; +import { InputField } from './InputField'; +import { useContractInfo, useMyContracts } from '@/hooks'; +import { shortenAddress, validateContractAddress } from '@/utils'; +import { useEffect, useState } from 'react'; +import { InputStatus } from './CodeIdField'; +import { useChainStore } from '@/contexts'; +import { useChain } from '@cosmos-kit/react'; + +type StatusDisplay = { + icon?: React.ReactNode; + text?: string; + textColor?: TextProps['color']; +}; + +const displayStatus = (status: InputStatus) => { + const statusMap: Record = { + loading: { + icon: , + text: 'Checking contract address...', + textColor: '$textSecondary', + }, + + success: { + icon: , + text: 'Valid contract address', + textColor: '$text', + }, + + error: { + icon: , + text: status.message || 'Invalid contract address', + textColor: '$textDanger', + }, + + init: {}, + }; + + return statusMap[status.state]; +}; + +type ContractAddressFieldProps = { + width?: string; + initialAddress?: string; + setContractAddress?: (address: string) => void; +}; + +export const ContractAddressField = ({ + width = '560px', + initialAddress, + setContractAddress, +}: ContractAddressFieldProps) => { + const [input, setInput] = useState(''); + const [status, setStatus] = useState({ state: 'init' }); + + const { selectedChain } = useChainStore(); + const { chain } = useChain(selectedChain); + const { refetch: fetchContractInfo } = useContractInfo({ + contractAddress: input, + enabled: false, + }); + const { data: myContracts = [] } = useMyContracts(); + + useEffect(() => { + if (initialAddress) { + setInput(initialAddress); + } + }, [initialAddress]); + + useEffect(() => { + setStatus({ state: 'init' }); + setContractAddress?.(''); + + if (input.length) { + const error = validateContractAddress(input, chain.bech32_prefix); + + if (error) { + return setStatus({ state: 'error', message: error }); + } + + setStatus({ state: 'loading' }); + + const timer = setTimeout(() => { + fetchContractInfo().then(({ data }) => { + if (!data) { + return setStatus({ + state: 'error', + message: 'This contract does not exist', + }); + } + + setStatus({ state: 'success' }); + setContractAddress?.(input); + }); + }, 500); + + return () => clearTimeout(timer); + } + }, [input, fetchContractInfo, chain.bech32_prefix]); + + const { icon, text, textColor } = displayStatus(status); + + return ( + + { + setInput(input); + }} + onSelectionChange={(value) => { + if (value) setInput(value as string); + }} + styleProps={{ width }} + > + {myContracts.map(({ address, contractInfo }) => ( + + + + {`${shortenAddress(address, 18)} (`} + + {`${contractInfo?.label || 'Unnamed'}`} + + {')'} + + + + ))} + + {status.state !== 'init' && ( + + {icon} + + {text} + + + )} + + ); +}; diff --git a/examples/chain-template/components/contract/QueryTab.tsx b/examples/chain-template/components/contract/QueryTab.tsx index 745c1f21..eb6f2b18 100644 --- a/examples/chain-template/components/contract/QueryTab.tsx +++ b/examples/chain-template/components/contract/QueryTab.tsx @@ -1,4 +1,6 @@ import { Box, Text } from '@interchain-ui/react'; +import { ContractAddressField } from './ContractAddressField'; +import { useEffect, useRef, useState } from 'react'; type QueryTabProps = { show: boolean; @@ -11,9 +13,51 @@ export const QueryTab = ({ initialAddress, clearInitAddress, }: QueryTabProps) => { + const [contractAddress, setContractAddress] = useState(''); + const [fieldWidth, setFieldWidth] = useState('560px'); + const containerRef = useRef(null); + + useEffect(() => { + const updateWidth = () => { + const newWidth = containerRef.current?.clientWidth; + if (newWidth) { + setFieldWidth(`${newWidth}px`); + } + }; + + updateWidth(); + const timeoutId = setTimeout(updateWidth, 0); + + window.addEventListener('resize', updateWidth); + + return () => { + clearTimeout(timeoutId); + window.removeEventListener('resize', updateWidth); + }; + }, [show]); + return ( - - Query Contract + + + Query Contract + + ); };