Skip to content

Instantly share code, notes, and snippets.

@Yuripetusko
Created July 19, 2024 02:19
Show Gist options
  • Save Yuripetusko/ed5c6ca6c3701da7246db16b5c066531 to your computer and use it in GitHub Desktop.
Save Yuripetusko/ed5c6ca6c3701da7246db16b5c066531 to your computer and use it in GitHub Desktop.
```
import { useAccount, useReadContract } from 'wagmi';
import { type Address, erc20Abi } from 'viem';
import { SKYBREACH_LAND_CHAIN_ID } from 'lib/constants/app';
type Props = {
tokenAddress?: Address;
allowedAddress?: Address;
enabled?: boolean;
chainId?: typeof SKYBREACH_LAND_CHAIN_ID;
};
export const useErc20Allowance = ({
tokenAddress,
allowedAddress,
enabled,
chainId = SKYBREACH_LAND_CHAIN_ID,
}: Props) => {
const { address } = useAccount();
const isEnabled = !!address && !!allowedAddress && !!enabled && !!chainId;
const {
data: allowance,
isLoading,
isFetching,
isError,
isSuccess,
refetch: refetchAllowance,
} = useReadContract({
chainId,
address: enabled ? tokenAddress : undefined,
args: isEnabled ? [address, allowedAddress] : undefined,
abi: erc20Abi,
functionName: 'allowance',
query: { enabled: isEnabled },
});
const refetch = enabled ? refetchAllowance : undefined;
return {
isLoading,
isFetching,
isSuccess,
isError,
allowance,
refetch,
};
};
```
```
import { useAccount } from 'wagmi';
import { useNativeTokenBalance } from './use-native-token-balance';
import type { SKYBREACH_LAND_CHAIN_ID } from 'lib/constants/app';
import { type Address } from 'viem';
import { useErc20Allowance } from 'lib/hooks/use-erc20-allowance';
import { formatUnitsToNumber } from 'lib/utils/token-unit-utils/format-units-to-number';
import type { ChainToken } from 'lib/types/chain-token';
import { useFormattedTokenBalance } from 'lib/hooks/use-formatted-token-balance';
type Props = {
balanceRequired: bigint;
currencyAddress?: Address;
enabled?: boolean;
chainId?: typeof SKYBREACH_LAND_CHAIN_ID;
contractToApprove?: Address;
isNativeCurrency?: boolean;
tokenInfo?: ChainToken;
};
const getIsSufficientAllowance = (
allowanceOwned: bigint | undefined,
allowanceRequired: bigint,
): boolean => {
return (
allowanceRequired === BigInt(0) || (!!allowanceOwned && allowanceOwned >= allowanceRequired)
);
};
export const getIsSufficientBalance = (
balanceOwned: bigint | undefined,
balanceRequired: bigint,
) => {
return balanceRequired === BigInt(0) || (!!balanceOwned && balanceOwned >= balanceRequired);
};
export const useIsSufficientTokenBalance = ({
balanceRequired,
currencyAddress,
enabled,
contractToApprove,
chainId,
isNativeCurrency,
tokenInfo,
}: Props) => {
const { address } = useAccount();
const {
allowance,
isLoading: isLoadingAllowance,
isFetching: isFetchingAllowance,
isError: isErrorAllowance,
refetch: refetchAllowance,
} = useErc20Allowance({
enabled: enabled && !isNativeCurrency,
chainId,
tokenAddress: currencyAddress,
allowedAddress: contractToApprove,
});
const {
tokenBalanceRaw,
refetch: refetchTokenBalance,
isLoading: isLoadingTokenBalance,
isFetching: isFetchingTokenBalance,
isError: isErrorTokenBalance,
} = useFormattedTokenBalance({
tokenAddress: currencyAddress,
account: address,
enabled: enabled && !isNativeCurrency,
});
const { data: balance, refetch: refetchNativeBalance } = useNativeTokenBalance(
{ account: address },
{ enabled: isNativeCurrency },
);
if (enabled && !isNativeCurrency && !tokenInfo) {
throw new Error('tokenInfo is required for erc20 currency');
}
const tokenBalance = isNativeCurrency ? balance?.value : tokenBalanceRaw?.value;
const isSufficientAllowance = getIsSufficientAllowance(allowance, balanceRequired);
const isSufficientBalance = getIsSufficientBalance(tokenBalance, balanceRequired);
const parsedBalanceRequired = formatUnitsToNumber(balanceRequired, tokenInfo?.decimals) ?? 0;
const isLoading = isLoadingAllowance || isLoadingTokenBalance;
// unlike isLoading, isFetching toggles when doing refetch
const isFetching = isFetchingAllowance || isFetchingTokenBalance;
const isError = isErrorAllowance || isErrorTokenBalance;
return {
parsedBalanceRequired,
isSufficientAllowance,
isSufficientBalance,
refetchAllowance,
refetchTokenBalance,
refetchNativeBalance,
isLoading,
isFetching,
isError,
tokenSymbol: tokenInfo?.symbol,
};
};
```
```
import React, { forwardRef } from 'react';
import { Button, type ButtonProps } from '@chakra-ui/react';
import { type Address, type erc20Abi, zeroAddress } from 'viem';
import type { SKYBREACH_LAND_CHAIN_ID } from 'lib/constants/app';
import type { DecodedContractError } from 'lib/wagmi/decode-evm-transaction-error-result';
import { formatCurrencyToDecimalPlaces } from 'lib/utils/token-unit-utils/numbers-and-math';
import { useErc20Approve } from 'lib/hooks/use-erc20-approve';
import type { ChainToken } from 'lib/types/chain-token';
import type { ButtonStylingProps } from 'components/common/modal/action-button';
type Props = {
currency: Address | undefined;
amountToApprove: number;
contractToApprove: Address;
onSuccess?: () => void;
onError?: (error: DecodedContractError<typeof erc20Abi>) => void;
displayRoundingPrecision?: number;
width?: ButtonProps['width'];
size?: ButtonProps['size'];
isLoading?: boolean;
isDisabled?: boolean;
chainId: typeof SKYBREACH_LAND_CHAIN_ID;
tokenInfo: ChainToken;
} & ButtonStylingProps;
export const Erc20ApproveButton = forwardRef<HTMLButtonElement, Props>(
(
{
currency,
amountToApprove,
contractToApprove,
onSuccess,
onError,
isLoading = false,
isDisabled = false,
displayRoundingPrecision = 4,
width = '100%',
size = 'md',
chainId,
tokenInfo,
},
ref,
) => {
const { setApproval, isLoading: isLoadingApprovalInternal } = useErc20Approve({
// AddressZero is here only for type safety, it should never actually be used, because of the `enabled` prop
currencyAddress: currency || zeroAddress,
contractToApprove,
amount: amountToApprove,
onSuccess,
onError,
enabled: !!currency,
chainId,
decimals: tokenInfo.decimals,
});
const amountString = formatCurrencyToDecimalPlaces(
amountToApprove,
tokenInfo.symbol,
displayRoundingPrecision,
);
return (
<Button
onClick={setApproval}
isLoading={isLoading || isLoadingApprovalInternal}
isDisabled={isDisabled}
textOverflow={'ellipsis'}
overflow={'hidden'}
width={width}
size={size}
ref={ref}
>
Approve {amountString}
</Button>
);
},
);
Erc20ApproveButton.displayName = 'Erc20ApproveButton';
```
```
import {
Box,
Button,
type ButtonOptions,
type ButtonProps,
type ThemingProps,
Tooltip,
} from '@chakra-ui/react';
import React, { type ReactNode } from 'react';
import { type Address, erc20Abi } from 'viem';
import type { SKYBREACH_LAND_CHAIN_ID } from 'lib/constants/app';
import { useIsSufficientTokenBalance } from 'lib/hooks/use-is-sufficient-token-balance';
import type { DecodedContractError } from 'lib/wagmi/decode-evm-transaction-error-result';
import { NetworkAndAccountCheckButtonWrapper } from 'components/common/network-and-account-check-button-wrapper';
import { Erc20ApproveButton } from 'components/common/erc20-approve-button';
import type { ChainToken } from 'lib/types/chain-token';
import type { StyleProps } from '@chakra-ui/styled-system';
export type ButtonStylingProps = ThemingProps<'Button'> & StyleProps & ButtonOptions;
type Props = {
children: ReactNode;
/**
* Callback to be called when the button is clicked.
*
* If the action requires spending tokens, this will be called after the token approval is successful.
*/
onClick: () => void;
/**
* Optional callback to be called after the token approval is successful.
*
* If async, the promise will be awaited before new allowance is checked and `onClick` callback is called.
*/
onApproveSuccess?: () => Promise<unknown> | unknown;
/**
* If the action requires spending tokens, this is the amount and currency that is required.
*
* If provided, the user will be prompted to approve the marketplace to spend the tokens before calling `onClick`.
*/
approvalDetails?: {
amount: bigint;
currency: Address | undefined;
/**
* Contract address, which approval state will checked and which contract user will be promted to approve.
*/
contractToApprove?: Address;
};
/**
* Used for checking the token allowance and balance.
*/
chainId: typeof SKYBREACH_LAND_CHAIN_ID;
/**
* Any additional loading conditions. This is already set internally for transactions in progress or network loading.
*/
isLoading?: boolean;
/**
* Any additional disabled conditions. This is already set internally for wrong network, unconnected wallet, or insufficient balance.
*/
isDisabled?: boolean;
/**
* A reason to show in the tooltip when the button is disabled.
*/
disableReason?: string;
/**
* Called whenever an error occurs on any of the steps.
*/
onError?: (error: Error | undefined) => void;
variant?: ButtonProps['variant'];
colorScheme?: ButtonProps['colorScheme'];
width?: ButtonProps['width'];
size?: ButtonProps['size'];
icon?: ButtonProps['leftIcon'];
hideWhenDisabled?: boolean;
connectButtonCopy?: string;
connectButtonStylingProps?: ButtonStylingProps;
isNativeCurrency?: boolean;
tokenInfo?: ChainToken;
};
export const ActionButton = ({
onClick,
onError,
onApproveSuccess: onApproveSuccessProp,
isLoading: isLoadingProp = false,
isDisabled: isDisabledProp = false,
disableReason: disableReasonProp,
variant,
colorScheme,
size = 'md',
width = '100%',
icon,
hideWhenDisabled = true,
connectButtonCopy,
connectButtonStylingProps = {},
approvalDetails,
chainId,
children,
isNativeCurrency,
tokenInfo,
}: Props) => {
const paymentCurrency = approvalDetails?.currency;
const isApprovalCheckRequired = !!approvalDetails && !!approvalDetails?.contractToApprove;
// TODO: consider calling onError when useIsSufficientTokenBalance returns an error
const {
parsedBalanceRequired,
isSufficientAllowance,
isSufficientBalance,
isLoading: isLoadingTokenAllowanceOrBalance,
isFetching: isFetchingTokenAllowanceOrBalance,
tokenSymbol,
refetchAllowance,
refetchTokenBalance,
} = useIsSufficientTokenBalance({
balanceRequired: approvalDetails?.amount || BigInt(0),
currencyAddress: paymentCurrency,
contractToApprove: approvalDetails?.contractToApprove,
enabled: isApprovalCheckRequired,
tokenInfo,
});
// This function will be called after the token approval transaction initiated by Erc20ApproveButton is successful.
const onApproveSuccess = async () => {
setTimeout(async () => {
await refetchAllowance?.();
refetchTokenBalance?.();
await onApproveSuccessProp?.();
}, 500);
};
let actionDisableReason: string | undefined = undefined;
if (disableReasonProp && isDisabledProp) {
actionDisableReason = disableReasonProp;
}
const approveIsDisabled = (isApprovalCheckRequired && !isSufficientBalance) || isDisabledProp;
const actionIsDisabled =
isDisabledProp ||
(!!approvalDetails?.amount && !isSufficientBalance) ||
(!!disableReasonProp && isApprovalCheckRequired && isSufficientBalance);
let approveDisableReason: string | undefined = undefined;
if (isApprovalCheckRequired && !isSufficientBalance) {
approveDisableReason = `Insufficient ${tokenSymbol ?? 'unknown token'} balance`;
}
const isApprovalRequired =
!!approvalDetails &&
!isSufficientAllowance &&
!isNativeCurrency &&
!!approvalDetails.contractToApprove;
if (disableReasonProp && isApprovalRequired && !approveDisableReason && !actionIsDisabled) {
approveDisableReason = disableReasonProp;
}
const buttonStylingProps: ThemingProps<'Button'> & StyleProps & ButtonOptions = {
variant,
colorScheme,
width: '100%',
size,
textOverflow: 'ellipsis',
overflow: 'hidden',
leftIcon: icon,
...connectButtonStylingProps,
};
const onApproveError = (error: DecodedContractError<typeof erc20Abi>) => {
onError?.(new Error(error.message));
};
return (
<NetworkAndAccountCheckButtonWrapper
hideWhenDisabled={hideWhenDisabled}
buttonProps={{ width, isLoading: isLoadingProp, ...buttonStylingProps }}
chainId={chainId}
connectButtonCopy={connectButtonCopy}
>
{({ isLoading: isNetworkLoading }) =>
isApprovalRequired && !!approvalDetails?.contractToApprove && !!tokenInfo ? (
<Tooltip label={approveDisableReason} isDisabled={!approveIsDisabled}>
<Box width={width}>
<Erc20ApproveButton
currency={paymentCurrency}
amountToApprove={parsedBalanceRequired}
contractToApprove={approvalDetails.contractToApprove}
onSuccess={onApproveSuccess}
onError={onApproveError}
isLoading={isLoadingTokenAllowanceOrBalance || isFetchingTokenAllowanceOrBalance}
isDisabled={approveIsDisabled}
width={width}
size={size}
displayRoundingPrecision={4}
chainId={chainId}
tokenInfo={tokenInfo}
{...buttonStylingProps}
/>
</Box>
</Tooltip>
) : (
<Tooltip
label={actionDisableReason || approveDisableReason}
isDisabled={!actionIsDisabled && !approveDisableReason}
>
<Box width={width}>
<Button
onClick={onClick}
isLoading={isLoadingProp || isNetworkLoading}
isDisabled={actionIsDisabled}
{...buttonStylingProps}
>
{children}
</Button>
</Box>
</Tooltip>
)
}
</NetworkAndAccountCheckButtonWrapper>
);
};
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment