antoncoding/monarch

View on GitHub
src/hooks/usePermit2.ts

Summary

Maintainability
A
25 mins
Test Coverage
import { useCallback, useMemo } from 'react';
import { Address } from 'abitype';
import moment from 'moment';
import { Chain } from 'viem/chains';
import { useReadContract, useSignTypedData } from 'wagmi';

import permit2Abi from '@/abis/permit2';
import { PERMIT2_ADDRESS } from '@/utils/permit2';
import { useAllowance } from './useAllowance';

type Props = {
  token: Address;
  chainId?: Chain['id'];
  user: Address | undefined;
  spender: Address | undefined;
  refetchInterval?: number;
  tokenSymbol?: string;
  amount: bigint;
};

/**
 * @param enabled Conditionally run the hook query
 * @param address Address for the contract
 * @param chainId Chain ID for the contract. If not provided, the chain ID from the connected wallet will be used.
 * @param refetchInterval Interval in milliseconds to refetch the contract data
 * @returns JsonMetadata
 */
export function usePermit2({
  user,
  chainId = 1,
  token,
  spender,
  refetchInterval = 10000,
  amount,
}: Props) {
  const {
    allowance: allowanceToPermit2,
    approveInfinite: authorizePermit2,
    isLoadingAllowance,
  } = useAllowance({
    user,
    spender: PERMIT2_ADDRESS,
    token,
    chainId,
    refetchInterval,
  });

  const { data: packedAllowance } = useReadContract({
    address: PERMIT2_ADDRESS,
    abi: permit2Abi,
    functionName: 'allowance', // read packed allowance
    args: [user, token, spender],
    chainId,
  });

  const { data: signature, signTypedDataAsync } = useSignTypedData({});

  const permit2Authorized = useMemo(
    () => allowanceToPermit2 && allowanceToPermit2 > amount,
    [allowanceToPermit2, amount],
  );

  const signForBundlers = useCallback(async () => {
    if (!user || !spender || !token) throw new Error('User, spender, or token not provided');

    const deadline = moment.now() + 600;

    const nonce = packedAllowance ? ((packedAllowance as number[])[2] as number) : 0;

    const permitSingle = {
      details: {
        token: token,
        amount: amount,
        expiration: deadline,
        nonce,
      },
      spender: spender,
      sigDeadline: BigInt(deadline),
    };

    // sign erc712 signature for permit2
    const sigs = await signTypedDataAsync({
      domain: {
        name: 'Permit2',
        chainId,
        verifyingContract: PERMIT2_ADDRESS,
      },
      types: {
        PermitDetails: [
          { name: 'token', type: 'address' },
          { name: 'amount', type: 'uint160' },
          { name: 'expiration', type: 'uint48' },
          { name: 'nonce', type: 'uint48' },
        ],
        // (PermitDetails details,address spender,uint256 sigDeadline)
        PermitSingle: [
          { name: 'details', type: 'PermitDetails' },
          { name: 'spender', type: 'address' },
          { name: 'sigDeadline', type: 'uint256' },
        ],
      },
      primaryType: 'PermitSingle',
      message: permitSingle,
    });

    return { sigs, permitSingle };
  }, [user, spender, token, chainId, packedAllowance, amount, signTypedDataAsync]);

  return {
    permit2Authorized,
    authorizePermit2,
    signForBundlers,
    isLoading: isLoadingAllowance,
    signature,
  };
}