diff --git a/CHANGELOG.md b/CHANGELOG.md index 7830a62..35b9e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Added +- [MINOR] Added `useWeb3WritableContract` to get a signer-bound (write-capable) contract instance directly, without a separate `useWeb3Contract` call or a manual `.connect()` +- [MINOR] Added optional `abi` param to `useWeb3Transaction` and a new `errorMessage` field on `Web3TransactionDetails`, decoding a transaction's custom-error revert data via the ABI when available +- [MINOR] Added `web3Units` helpers: `Web3MaxUint256`, `formatWeb3Units` (null-safe `formatUnits`), and `parseWeb3Units` (safe-fallback or throwing `parseUnits`) + ### Changed ### Removed diff --git a/src/index.ts b/src/index.ts index 4cfb211..c7aaae9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from './model'; export * from './Web3AccountContext'; export * from './useWeb3Contract'; export * from './useWeb3Transaction'; +export * from './web3Units'; diff --git a/src/useWeb3Contract.ts b/src/useWeb3Contract.ts index 0e56420..0788179 100644 --- a/src/useWeb3Contract.ts +++ b/src/useWeb3Contract.ts @@ -2,7 +2,7 @@ import React from 'react'; import { Contract as EthersContract } from 'ethers'; -import { Web3Contract, Web3ContractInterface } from './model'; +import { Web3Contract, Web3ContractInterface, Web3Signer } from './model'; import { useWeb3, useWeb3ChainId } from './Web3AccountContext'; @@ -24,3 +24,21 @@ export const useWeb3Contract = (contractChainIdAddressMap: Record, abi: Web3ContractInterface, signer: Web3Signer | null | undefined): Web3Contract | null | undefined => { + const chainId = useWeb3ChainId(); + const contract = React.useMemo((): Web3Contract | null | undefined => { + if (chainId === undefined || signer === undefined) { + return undefined; + } + if (chainId === null || signer === null) { + return null; + } + const contractAddress = contractChainIdAddressMap[chainId]; + if (!contractAddress) { + return null; + } + return new EthersContract(contractAddress, abi, signer); + }, [chainId, abi, contractChainIdAddressMap, signer]); + return contract; +}; diff --git a/src/useWeb3Transaction.ts b/src/useWeb3Transaction.ts index 05b3bfb..3968455 100644 --- a/src/useWeb3Transaction.ts +++ b/src/useWeb3Transaction.ts @@ -1,6 +1,8 @@ import React from 'react'; -import { Web3TransactionReceipt, Web3TransactionResponse } from './model'; +import { Interface, isCallException } from 'ethers'; + +import { Web3ContractInterface, Web3TransactionReceipt, Web3TransactionResponse } from './model'; export type TransactionPromise = Promise; @@ -8,21 +10,39 @@ export interface Web3TransactionDetails { transactionPromise: TransactionPromise | null; transaction: Web3TransactionResponse | null; error: Error | null; + errorMessage: string | null; receipt: Web3TransactionReceipt | null; } -export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPromise: TransactionPromise | null) => void, () => void, () => void] => { +const decodeErrorMessage = (newError: unknown, abi?: Web3ContractInterface): string => { + if (abi && isCallException(newError) && newError.data) { + try { + const parsedError = new Interface(abi).parseError(newError.data); + if (parsedError) { + return parsedError.name; + } + } catch { + // Revert data present but not decodable with this ABI (e.g. selector from a different contract) + } + } + return newError instanceof Error ? newError.message : String(newError); +}; + +export const useWeb3Transaction = (abi?: Web3ContractInterface): [Web3TransactionDetails, (newTransactionPromise: TransactionPromise | null) => void, () => void, () => void] => { const [transactionPromise, setTransactionPromise] = React.useState(null); const [transaction, setTransaction] = React.useState(null); const [error, setError] = React.useState(null); + const [errorMessage, setErrorMessage] = React.useState(null); const [receipt, setReceipt] = React.useState(null); const clearError = React.useCallback((): void => { setError(null); + setErrorMessage(null); }, []); const clearTransaction = React.useCallback((): void => { setError(null); + setErrorMessage(null); setReceipt(null); setTransactionPromise(null); setTransaction(null); @@ -30,6 +50,7 @@ export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPr const setNewTransactionPromise = React.useCallback((newTransactionPromise: TransactionPromise | null): void => { setError(null); + setErrorMessage(null); setReceipt(null); setTransactionPromise(newTransactionPromise); }, []); @@ -43,9 +64,10 @@ export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPr setTransaction(newTransaction); } catch (newError: unknown) { setError(newError as Error); + setErrorMessage(decodeErrorMessage(newError, abi)); } setTransactionPromise(null); - }, [transactionPromise]); + }, [transactionPromise, abi]); React.useEffect((): void => { waitForTransactionPromise(); @@ -60,10 +82,11 @@ export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPr setReceipt(newReceipt); } catch (newError: unknown) { setError(newError as Error); + setErrorMessage(decodeErrorMessage(newError, abi)); setReceipt(null); } setTransaction(null); - }, [transaction]); + }, [transaction, abi]); React.useEffect((): void => { waitForTransaction(); @@ -74,6 +97,7 @@ export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPr transactionPromise, transaction, error, + errorMessage, receipt, }, setNewTransactionPromise, diff --git a/src/web3Units.ts b/src/web3Units.ts new file mode 100644 index 0000000..b31d4fa --- /dev/null +++ b/src/web3Units.ts @@ -0,0 +1,16 @@ +import { formatUnits as ethersFormatUnits, MaxUint256 as EthersMaxUint256, parseUnits as ethersParseUnits } from 'ethers'; + +export const Web3MaxUint256 = EthersMaxUint256; + +export const formatWeb3Units = (value: bigint | null | undefined, decimals: number, fallback: string = ''): string => (value == null ? fallback : ethersFormatUnits(value, decimals)); + +export const parseWeb3Units = (value: string, decimals: number, fallback?: T): bigint | T => { + try { + return ethersParseUnits(value, decimals); + } catch (error) { + if (fallback === undefined) { + throw error; + } + return fallback; + } +};