Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './model';
export * from './Web3AccountContext';
export * from './useWeb3Contract';
export * from './useWeb3Transaction';
export * from './web3Units';
20 changes: 19 additions & 1 deletion src/useWeb3Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


Expand All @@ -24,3 +24,21 @@ export const useWeb3Contract = (contractChainIdAddressMap: Record<number, string
}, [web3, chainId, abi, contractChainIdAddressMap]);
return contract;
};

export const useWeb3WritableContract = (contractChainIdAddressMap: Record<number, string>, 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;
};
32 changes: 28 additions & 4 deletions src/useWeb3Transaction.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,56 @@
import React from 'react';

import { Web3TransactionReceipt, Web3TransactionResponse } from './model';
import { Interface, isCallException } from 'ethers';

import { Web3ContractInterface, Web3TransactionReceipt, Web3TransactionResponse } from './model';

export type TransactionPromise = Promise<Web3TransactionResponse>;

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<TransactionPromise | null>(null);
const [transaction, setTransaction] = React.useState<Web3TransactionResponse | null>(null);
const [error, setError] = React.useState<Error | null>(null);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [receipt, setReceipt] = React.useState<Web3TransactionReceipt | null>(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);
}, []);

const setNewTransactionPromise = React.useCallback((newTransactionPromise: TransactionPromise | null): void => {
setError(null);
setErrorMessage(null);
setReceipt(null);
setTransactionPromise(newTransactionPromise);
}, []);
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -74,6 +97,7 @@ export const useWeb3Transaction = (): [Web3TransactionDetails, (newTransactionPr
transactionPromise,
transaction,
error,
errorMessage,
receipt,
},
setNewTransactionPromise,
Expand Down
16 changes: 16 additions & 0 deletions src/web3Units.ts
Original file line number Diff line number Diff line change
@@ -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 = <T = never>(value: string, decimals: number, fallback?: T): bigint | T => {
try {
return ethersParseUnits(value, decimals);
} catch (error) {
if (fallback === undefined) {
throw error;
}
return fallback;
}
};
Loading